--- title: 工具与审批 description: 极简内置工具集的设计与执行契约、Environment 统一收尾规则,以及逐调用审批与 Trace 审计。 --- ## 设计取向 PenguinHarness 刻意维持一个极小的内置工具集:Shell 是通用接口,文件的读取、写入、编辑全部经由 `exec_command` 完成,不设专门的文件工具。工具越少,注入的 schema 越少,Token 开销越小,模型误调用的概率也越低。 ## 执行契约 所有内置工具实现同一个 `BuiltinTool` 接口(`packages/core/src/environment/tools/types.ts`): ```ts interface BuiltinTool { name: string; definition: ToolDefinitionConfig; execute( args: Record, ctx: ToolExecutionContext, ): AsyncGenerator; } 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 协议](/omni-message)。 ## 配置字段 每个工具由一条 `ToolDefinitionConfig` 描述: | 字段 | 说明 | | --- | --- | | `name` | 工具名,对应模型产出的 `tool_call.name` | | `description` | 提供给模型的工具说明 | | `parameters` | 参数 JSON Schema | | `permission` | `"r"` 只读 / `"rw"` 读写 | | `forModel` | `"vision"` / `"text-only"`:按 Session 模型类别装配;缺省对所有模型可用 | | `timeoutMs` | 单次调用超时(ms),默认 120000;`<=0` 关闭 | | `maxOutputLength` | 输出长度上限(字符);`<=0` 关闭 | ## 内置工具 共 6 个内置工具(装配入口 `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、轮询输出 | | `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 模型) | ### 命令会话 `exec_command` 先在前台等待;命令超过 `yield_time_ms` 仍未结束时转入后台,返回已有输出和一个 `process_id`,之后用 `input_command` 驱动: ```text exec_command(cmd) ├─ 前台窗口(yield_time_ms,默认 60000)内结束 ──► 完整输出 + 退出码 └─ 未结束 ──► 转入后台,返回已有输出 + process_id │ input_command(process_id[, chars]) ──► 写 stdin / 发 Ctrl-C / 轮询 └─ 循环驱动,直至命令退出 ``` 两个工具的参数(明确键名): ```ts // exec_command { cmd: string; // 必填:要执行的 shell 命令 workdir?: string; // 工作目录;缺省为 Workspace 根,相对路径按其解析 yield_time_ms?: number; // 前台等待时长;默认 60000,最小 250,上限受工具超时约束 } // input_command { process_id: string; // 必填:exec_command 返回的命令会话 id chars?: string; // 写入 stdin 的字符;单独发送 "\u0003" 传递 Ctrl-C;缺省仅轮询 yield_time_ms?: number; // 等待时长;有写入默认 250,空轮询默认 5000 } ``` ### Subagent `run_subagent` 把一段能一次说清的子任务交给子 Agent 执行,同样是两段式:前台窗口(默认 300000ms)过后转入后台并返回 `subagent_id`,由 `input_subagent` 轮询或追加 Prompt;子 Agent 的待审批项会在轮询等待期间浮出。 ```ts // run_subagent { prompt: string; // 必填:完整的子任务(含全部上下文与期望的最终产出) agent_id?: string; // 子 Agent;缺省复用当前 Agent model_id?: string; // 子 Session 模型;缺省继承父 Session 的模型 yield_time_ms?: number; // 前台等待时长;默认 300000 } // input_subagent { subagent_id: string; // 必填:run_subagent 返回的后台 Subagent id prompt?: string; // 追加任务,仅在子 Session 空闲时接受;缺省仅轮询 yield_time_ms?: number; // 等待时长;有追加默认 300000,空轮询默认 10000 } ``` - 深度上限为 1:Subagent 不能再派生 Subagent。 - 子 Session 跟随父 Session:模型(除非以 `model_id`/`provider` 显式指定)、thinking level 与 Workspace 均继承父级,而非 Project 默认值。 - 子 Session 继承父 Agent 的审批回调,审批模式随父生效。 - 子 Session 拥有独立 Trace,父 Trace 以 `subagent` 指针事件链接;子消息带 `origin` 标记回流到父级消息流。见 [Session 与 Trace](/sessions-and-traces)。 ### 图像工具 `read_image` 与 `describe_image` 互斥,按 Session 模型的 vision 标记二选一装配。两者都接受 http(s) URL 或 Workspace 路径,支持 png/jpeg/gif/webp,不超过 5MB。text-only 模型走 `describe_image`:图片连同提问转交 Project 配置的 `vision_model`,其文字回答即工具输出。见 [模型与 Provider](/models)。 ```ts // read_image(vision 模型) { source: string; // 必填:http(s) URL,或 Workspace 内的文件路径 } // describe_image(text-only 模型) { source: string; // 必填:同上 prompt?: string; // 要对图片提出的问题;缺省为详细描述 } ``` ### 后台会话上限 | 会话类型 | 上限 | 淘汰策略 | | --- | --- | --- | | 命令会话 | 64 | 满时优先淘汰已退出者,否则对空闲会话按 LRU 淘汰 | | Subagent 会话 | 8 | 只淘汰已完成者;运行中的从不淘汰,无空位则拒绝派生 | ## 审批 每个完整的 `tool_call` 触发且只触发一次审批决策: ```ts type ApproveFn = (toolCall: OmniMessage) => 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 运行循环](/agent-loop) 的工具执行阶段。 ## 自定义与 MCP `system_config.yaml` 的 `tools.builtin` 数组以 `ToolDefinitionConfig` 同构条目声明工具集。注意语义是**整体替换而非合并**:整段省略时使用完整默认工具集;一旦写出,默认列表即被替换,要保留的每个工具都必须携带完整定义(含 `parameters` JSON Schema——工具的参数 schema 完全来自配置)。`tools.mcpServers` 承载 MCP Server 配置(name + config)——具体 MCP 工具的枚举由后续适配层接管,当前仅保留配置位。见 [配置参考](/configuration)。 ```yaml tools: # 写出 builtin 即整体替换默认工具集(此例刻意只保留一个最小工具集)。 builtin: - name: exec_command description: Run a shell command in the workspace. permission: rw timeoutMs: 120000 maxOutputLength: 16000 # parameters: 必须携带完整 JSON Schema(默认定义见 # packages/core/src/state/default-config.ts),此处从略。 mcpServers: [] ```