Kilo 核心工具架构解析:Tool 表示、Location 注册与结算机制深度指南
【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode
导读
本文以 packages/core/src/tool/AGENTS.md 为核心脉络,系统讲解 Kilo(开源编码 Agent 平台)Core 包中工具子系统的整体架构:一种不透明的规范 Tool 表示、进程级与 Location 级两层注册、生效查找(effective lookup)与结算(settlement)机制。读完本文,你将理解内置工具与应用工具如何共用同一类型、Location 注册如何覆盖应用注册、权限过滤与执行授权如何分离,以及工具输出的通用模型侧收口(bounding)与受管存储(managed output)边界落在哪里。
一、总体架构:一个本地工具表示 + 进程/Location 双层注册
packages/core/src/tool/目录拥有 Core 中唯一的本地工具表示,以及完整的注册、生效与结算链路。该目录下的核心文件构成四个层次:
| 文件 | 职责 |
|---|---|
| tool.ts | 定义不透明的规范Tool.make(...)值(description / input / output / execute / toModelOutput) |
| application-tools.ts | 存储进程作用域(process-scoped)的应用注册 |
| tools.ts | 暴露仅含注册能力的Tools.Service视图,供 Location 生产者使用 |
| registry.ts | 只存放规范工具,叠加 Location 注册、派生定义、调用工具并施加通用输出收口 |
从源码结构看,这套设计最重要的约束是单一执行入口类型:文档明确要求不要新增第二种可执行条目类型、registry 自有执行器、授权回调、输出路径回调或遗留归一化路径(legacy normalization path)。也就是说,所有可执行工具——无论应用工具还是内置工具——都必须以同一种Tool.make值存在。
对应到具体内置实现,builtins.ts 只组合随发布提供的 Location 作用域内置工具变换,其deps列出的正是bash、edit、apply_patch、write、glob、grep、question、read、skill、todowrite、webfetch、websearch等工具节点。文件头部注释同时指出:动态 MCP 与插件工具后续走独立的 scoped 规范注册,而不是进入这份静态内置列表。
二、构造(Construction):不透明 Tool 值与其运行时细节
2.1Tool.make的配置面
Tool.make接收一个Config,其核心字段在 tool.ts 中定义:
description:模型可见的工具描述;input/output:均基于 Effect Schema 的编解码器,构成工具输入/输出的唯一事实来源;structured/toStructuredOutput(可选):用于派生结构化输出;execute:真正的执行函数,签名是(input, context) => Effect<Output, ToolFailure>;toModelOutput(可选):把已验证的输出投影成模型可读的Content数组(文本或内联文件)。
而工具值是不透明的:调用方拿到的只是一个Object.freeze({})的Definition对象,其 codec、执行器、定义派生逻辑与目录权限声明全部被隐藏在WeakMap<AnyTool, Runtime>中(tool.ts),外部无法直接触碰。Tool.definition(name, tool)、Tool.settle(tool, call, context)、Tool.permission(tool, name)、Tool.withPermission(tool, permission)是仅有的几个公开操作入口。
2.2 名字校验与权限装饰
Tool.validateName用正则/^[A-Za-z][A-Za-z0-9_-]{0,63}$/校验工具名,失败时抛出Tool.RegistrationError。Tool.withPermission通过复制 runtime 并覆写permission字段来装饰工具(tool.ts),装饰后的值依然是同一个不透明类型。
2.3 规范调用上下文的权限源
Location 作用域的内置工具层在构造期间获取PermissionV2.Service及其所需的其他 Location 服务,执行器捕获这些服务。权限源(permission source)总是从规范的调用上下文构造:
const source = { type: "tool" as const, messageID: context.assistantMessageID, callID: context.toolCallID, }这一代码模式在 bash.ts、edit.ts、write.ts、apply-patch.ts 中完全一致——Context结构(sessionID、agent、assistantMessageID、toolCallID)在 tool.ts 中统一定义。
2.4 错误翻译纪律
文档强调:叶子工具自行负责解析、权限与副作用顺序,并且只把预期的类型化错误翻译成ToolFailure,不要使用catchCause——因为中断(interruption)和缺陷(defect)必须原样存活。这正是 Effect 生态中"错误是值、缺陷是缺陷"的边界体现:工具内部可以用Effect.mapError把业务错误包装成ToolFailure(如 bash.ts 的Unable to execute command),但绝不能把中断/缺陷误吞为普通工具失败。
三、注册(Registration):两层作用域与覆盖规则
3.1 两条注册通路
- 内置工具通过
Tools.Service.register({ [name]: tool })注册,例如 bash.ts、edit.ts; - 应用工具通过
ApplicationTools.Service.register(...)注册,对外公开为opencode.tools.register(...)。
ApplicationTools.Service是**进程级(process-scoped)**的,由所有 Location 共享;而ToolRegistry.Service是 **Location 级(Location-scoped)**的。文档明确禁止把 registry 做成进程全局,也禁止为每个 Location 单独构造应用工具服务——保证应用工具注册一次、处处可见,而 Location 内注册天然隔离。
3.2 覆盖(shadowing)语义
两层注册的生效规则(来自 registry.ts 的实现):
- 最新活跃的同层注册胜出:Location 本地注册以
Map<string, Array<Registration>>栈式存储,查找时取entries.at(-1); - 关闭任一注册只移除该注册,并暴露下一个活跃注册:注册通过
Effect.addFinalizer登记清理逻辑(registry.ts),Scope 关闭时只移除当前 token 对应的条目; - Location 注册优先于应用注册:
settleWith中先查local栈,查不到才回落到applications.entries(); - 调用在结算开始时捕获生效工具:
materialize把应用注册与当前 Location 注册合并成一张不可变快照,后续settle都基于这张快照,避免注册中途变更导致执行不一致。
此外还有一个"陈旧调用"保护:settleWith支持传入advertised(广告过的注册身份),如果实际生效的注册身份与广告不一致,直接返回Stale tool call: <name>错误(registry.ts),防止模型基于旧定义发起调用。
3.3 注册期的并发与原子性
ToolRegistry.register内部包裹在Effect.uninterruptible中(registry.ts),确保一批工具的注册/清理是原子的。注册前还会用Effect.forEach批量执行validateName,任一名字非法则整批失败。
四、权限(Permissions):定义过滤 ≠ 执行授权
这是本架构最容易误解的一点,值得单独展开:
- registry 不依赖
PermissionV2.Service,也不做执行授权。从 registry.ts 的Service声明可见,ToolRegistry的依赖只有ApplicationTools与ToolOutputStore; - 有一个仅供内置工具使用的内部操作,会给工具附加一个 permission action,目的纯粹是为了保留整工具定义过滤(whole-tool definition filtering)能力,它不是公开
Tool.make的一部分; - 大多数工具默认以其注册名为 action;而
edit、write、apply_patch三个工具统一声明共享的editaction——在 edit.ts、write.ts、apply-patch.ts 中均可看到Tool.withPermission(tool, "edit")的用法。
定义过滤是目录可见性(catalog visibility),不是执行授权。调用如果走到了结算,仍会执行捕获到的叶子策略(leaf policy)。在实现上,materialize用whollyDisabled(action, permissions)判断:只有命中resource === "*" && effect === "deny"的通配规则时,才把工具从definitions中剔除(registry.ts);而真正的授权发生在叶子工具执行时的permission.assert(...)调用——例如 bash.ts 对命令本身、edit.ts 对编辑资源。
此外,叶子工具的解析顺序统一为:先用LocationMutation.resolve解析路径 → 若目标在外部目录则先断言external_directory权限 → 再断言自身 action(如edit)。bash.ts、edit.ts、write.ts、apply-patch.ts 都是这一模式。
五、输出(Output):settle是唯一的执行与收口边界
5.1 结算(settlement)流程
文档明确指出:ToolRegistry.Materialization.settle是唯一的执行与通用模型输出收口(generic model-output bounding)边界,并且拥有受管保留路径(managed retention paths)。
结合 registry.ts 与 tool.ts 的实现,settle的完整链路是:
- 按调用名查找注册(Location 栈优先,回落应用注册);
Tool.settle用Schema.decodeUnknownEffect(config.input)解码调用输入,失败映射为Invalid tool input: ...的ToolFailure;- 执行
config.execute(input, context); - 用
Schema.encodeEffect(config.output)编码输出,失败映射为Tool returned an invalid value for its output schema: ...; - 若配置了
structured+toStructuredOutput,再编码结构化输出; - 调用
toModelOutput生成Content数组——文本部分直接透传,file类型部分编码为data:<mime>;base64,<data>的 data URI(tool.ts); - registry 捕获
LLM.ToolFailure并转成错误结果,然后把输出交给ToolOutputStore.bound做通用收口(registry.ts)。
5.2 输出收口与受管存储
ToolOutputStore(tool-output-store.ts)定义了三个关键常量:
MAX_LINES = 2_000MAX_BYTES = 50 * 1024RETENTION = Duration.days(7)
bound会对超限输出做"首尾采样"式截断(preview保留头部一半、尾部一半行数),超过阈值的部分落盘到MANAGED_DIRECTORY = "tool-output"目录,返回的outputPaths指向受管文件。结算结果Settlement因此包含result、可选output与可选outputPaths三部分。
值得注意的是,edit与apply_patch两个工具还做了工具侧的自压缩:当序列化输出超过ToolOutputStore.MAX_BYTES时,compact会把files数组裁剪为仅保留additions/deletions/file/status字段、丢弃巨大的 diff 文本(edit.ts、apply-patch.ts),从而在不隐藏正常大小 diff 预览的前提下,保持持久化/SSE 工具记录的体积有界。
5.3 生产者捕获限制是另一回事
文档特别澄清:生产者(producer)的捕获限制与模型输出收口是分离的。以 Bash 为例:
- 它自己维护
AppProcess.maxOutputBytes(在 bash.ts 中即MAX_CAPTURE_BYTES = 1024 * 1024); - 它会在输出里如实报告 stdout/stderr 捕获丢失(输出被截断时追加
[output capture truncated at the in-memory safety limit]说明); - 但它不做模型输出截断,也不返回受管
outputPath——那是settle边界的事。
也就是说,Bash 的超时(默认DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000,上限MAX_TIMEOUT_MS = 10 * 60 * 1_000)与内存捕获上限属于进程层约束,而模型看到的最终输出形状由 registry 的settle+ToolOutputStore.bound统一决定。edit/apply_patch的compact与ToolOutputStore的preview分别位于工具内与结算边界,两层共同保证输出记录可控。
六、当前缺口(Current Gaps):已知的演进方向
文档如实记录了三个尚未完成的设计点,对于理解代码现状至关重要:
- 插件启动(plugin boot)尚未重构为通过
Tools.Service注册规范工具——文档明确要求不要作为叶子迁移的一部分顺带重设计它; - MCP 与未来的 Session 级注册仍缺少显式的规范注册设计——
builtins.ts的注释也印证了"动态 MCP 与插件工具稍后使用独立 scoped 规范注册"的规划; - 公开 Session 结果形状目前暴露了受管的
outputPaths——完整的存储封装需要未来的不透明受管输出引用设计(opaque managed-output reference)才能实现。
这三个缺口都指向同一个方向:当前"进程级应用注册 + Location 级内置注册 + registry 结算"的骨架已经稳定,而插件、MCP 与 Session 级扩展点正在向同一套规范工具模型收敛。
七、小结:架构设计的关键约束一览
| 关注点 | 结论 | 源码位置 |
|---|---|---|
| 工具表示 | 唯一不透明的Tool.make值,应用/内置共用 | tool.ts |
| 注册层级 | 应用进程级、Location 级,Location 优先、最新胜出、可关闭回退 | application-tools.ts、registry.ts |
| 公开注册 API | 内置走Tools.Service.register,应用走opencode.tools.register | tools.ts、application-tools.ts |
| 权限 | registry 不授权;定义过滤只影响目录可见性;edit/write/apply_patch共享editaction | registry.ts、edit.ts |
| 输出边界 | settle是唯一执行与模型输出收口点;ToolOutputStore管理受管输出路径 | registry.ts、tool-output-store.ts |
| 生产者限制 | 如 Bash 的捕获上限仅报告丢失,不截断模型输出、不返回 outputPath | bash.ts |
| 已知缺口 | 插件 boot、MCP/Session 级注册、Session 结果形状封装 | AGENTS.md |
这套架构的核心价值在于:用"一个规范工具类型 + 两层作用域注册 + 唯一结算边界"把工具系统的可扩展性与可治理性统一起来——应用开发者只需opencode.tools.register即可接入,内置叶子共享同一执行与权限纪律,而输出形状则由settle统一收口,为插件、MCP 等后续扩展点保留了清晰的挂载位置。
【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考