// Package tools 定义工具系统的核心接口和注册表. // // 工具系统的类型定义和接口. // 原项目的工具系统有以下问题: // - Tool 定义和 UI 渲染耦合(renderToolResultMessage 直接返回 JSX) // - 并发安全性通过每个工具的 isConcurrencySafe() 方法判断,散落各处 // - 权限检查混在工具内部和外部两层 // // Go 版本的设计: // - Tool 接口只关心执行逻辑,不关心 UI // - 并发安全性和只读性作为元数据声明,由编排器统一调度 // - 权限检查完全由 PermissionEngine 负责,工具不参与 package tools import ( "context" "encoding/json" ) // Tool 是所有工具必须实现的接口. // 对应原项目中 Tool 泛型类型. // // Go 版本简化为统一的 JSON 输入/输出,避免泛型复杂度. // 类型安全由每个工具内部的 json.Unmarshal 保证. // // Shape: synchronous callback. Engine dispatches via Execute with a // JSON input and blocks for *Result; custom tool authors inject via // engine.Config.Tools. Description / InputSchema / Name are pull-side // metadata read by engine at registration / prompt assembly time. // // 形态: 同步回调. 引擎经 Execute 传 JSON 输入阻塞等 *Result; 自定义工具 // 作者经 engine.Config.Tools 注入. Description / InputSchema / Name 是 // pull 侧元数据, 引擎在注册 / 拼 prompt 时读. type Tool interface { // Name 返回工具的唯一标识符. Name() string // Description 返回工具的描述(提供给模型的说明). // 可以是动态的,根据当前上下文返回不同内容. Description(ctx context.Context) string // InputSchema 返回工具的 JSON Schema 输入定义. // 模型根据此 schema 生成工具调用参数. InputSchema() json.RawMessage // Execute 执行工具逻辑. // input 是模型生成的 JSON 参数(已通过 schema 验证). // 返回 Result 和可能的错误. // // ctx 携带取消信号,对应原 TS 项目的 AbortController. // progress 回调用于报告长时间任务的进度. Execute(ctx context.Context, input json.RawMessage, progress ProgressFunc) (*Result, error) } // Metadata 是工具的元数据声明. // 工具可以选择实现 MetadataProvider 接口来声明这些属性. // 如果不实现,编排器使用保守默认值(不可并发,非只读,非破坏性). // // 对应原项目中 isConcurrencySafe(),isReadOnly(),isDestructive() 等方法. type Metadata struct { // ConcurrencySafe 表示该工具是否可以与其他工具并行执行. // true: Glob,Grep,FileRead 等只读工具 // false: FileEdit,FileWrite,Bash(可能有副作用)等 ConcurrencySafe bool // ReadOnly declares this tool has no side effects (pure queries / // read-only FS access). Informational metadata: external SDK / TUI / // audit consumers render a "read-only" label; evolve's tool generator // may auto-set it. // // Not part of permission decisions -- permission path is driven by // PermissionClass (above). No in-tree consumer reads this field for // permission logic today. // // ReadOnly 声明该工具不产生副作用 (纯查询 / 只读 FS 访问). // Informational 元数据: 外部 SDK / TUI / audit 消费层据此渲染 "只读" // 标签, evolve 工具生成器也可据此自动设置. // // 不参与权限决策 -- 权限路径由 PermissionClass 驱动 (见上方). // 本字段当前无 in-tree 消费者读取做权限判断. ReadOnly bool // Destructive declares the tool may cause irreversible damage (rm -rf, // git push --force, DROP TABLE). Informational metadata: audit / UI // render warning icons; reports count "N destructive calls this session". // // Not part of permission decisions -- same constraint as ReadOnly. // // Destructive 声明该工具可能造成不可逆破坏 (rm -rf / git push --force / // DROP TABLE 等). Informational 元数据: audit / UI 据此渲染警告图标, // 审计报表统计 "本 session 破坏性调用 N 次". // // 不参与权限决策 -- 约束同 ReadOnly. Destructive bool // Aliases 工具的别名列表(向后兼容用). Aliases []string // SearchHint 搜索提示词(3-10 字),用于 ToolSearch 延迟加载时的匹配. SearchHint string // PermissionClass 声明工具的权限检查类型. // 权限引擎据此分派正确的安全检查逻辑,避免权限系统硬编码工具名. // // 升华改进(ELEVATED): P0-2 修复核心--工具自描述权限语义, // 第三方工具注册时 tools.Registry 自动调用 permission.RegisterToolClass. // 有效取值(见 permission 包中的 PermClass* 常量): // "bash" - 命令执行类(子命令分割 + 危险命令检测) // "file" - 文件操作类(路径权限 + 危险路径检测) // "webfetch" - Web 请求类(域名权限检查) // "readonly" - 只读类(直接放行) // "generic" - 通用(默认,规则匹配后 Ask) // "" - 未声明,权限系统使用内置 fallback(向后兼容) PermissionClass string // AuditOperation 声明工具的审计操作类型. // 用于 AuditObserver 将工具事件翻译为 AuditEntry.Operation 字段. // // 升华改进(ELEVATED): P1 L1191 修复 - 与 PermissionClass 同源模式, 工具自描述 // 跨切面语义 (权限 + 审计), 消费者 (flysafe / flyto-platform-common / 新工具作者) // 注册自定义工具时不需要 fork 引擎修改 audit_observer.operationFromTool 的 // switch.例如第三方 "DatabaseWrite" / "SlackSend" / "K8sApply" 工具通过 // Metadata{AuditOperation: "write"} 声明, 审计记录立即正确. // // 常用取值 (任意自定义字符串也合法, 由消费者与其审计后端约定): // "write" - 写入 (文件写, DB 写, 消息发送) // "edit" - 修改 (文件编辑, DB 更新) // "read" - 读取 (文件读, DB 查询, glob, grep) // "execute" - 命令执行 (bash, 子进程) // "delete" - 删除 (文件删, DB 删) // "invoke" - 通用调用 (默认值, 无副作用) // "" - 未声明, AuditObserver 回退到启发式 (基于工具名匹配) // // 替代方案 A: <保留硬编码 switch, 不加 Metadata 字段> - 否决: // 违反开闭原则, 消费者加新工具必须 fork 引擎. // 替代方案 B: <新增全局 RegisterAuditOperation(name, op) 函数> - 否决: // 引入平行于 Metadata 的第二种注册路径, 与既有 PermissionClass 风格不一致. // 替代方案 C: - 否决: // 消费者要事先知道所有工具名, 插件动态注册场景不适用. AuditOperation string // RequiresCheckpoint 表示该工具执行前需要等待外部确认. // // 升华改进(ELEVATED): 早期实现 中"Human checkpoint"只是提示词里的文字建议, // 模型自己决定要不要暂停--不可靠(模型可能忽略). // 我们在工具声明层强制暂停:RequiresCheckpoint=true 的工具, // 引擎在执行前必须经过 CheckpointHandlerFn 放行,handler 未注册默认拒绝. // // 适用场景(不可逆操作): // - 发送消息(邮件,IM,短信) // - 删除数据(DROP TABLE,rm -rf) // - 生产部署(kubectl apply,terraform apply) // - 资金操作(转账,支付) // // 替代方案:<在 prompt 中写"执行前问用户"> // - 否决:依赖模型遵守,不可靠;且无法在 SDK 嵌入模式下触发程序级回调. RequiresCheckpoint bool // RequiresReverseThinking flags this tool as one whose call benefits // from a reverse-thinking pass before execution: producing a // counterfactual.Deliverable (hidden assumptions / failure scenarios / // verdict / verdict_reason) that is persisted alongside the call for // audit and evolve.Reflector replay. // // Boundary with RequiresCheckpoint: RequiresCheckpoint asks "should a // human approve this call?"; RequiresReverseThinking asks "should the // agent challenge its own recommendation before this call?". Both can // be true simultaneously (high-stakes calls deserve both); core does // not impose ordering -- the consuming hook chain (e.g. // platform/common/safetychain) decides whether reverse-think runs // before / after / instead-of the human checkpoint. // // Suitable when (non-trivial design space, not every irreversible op): // - Tool selects between competing strategies (route planning, // carrier choice, policy parameter) // - Tool encodes a decision whose failure mode is non-obvious // (settlement matching, fraud judgment) // - Tool calls a downstream system whose schema may shift // (third-party API, partner integration) // // Avoid when: the tool is a pure mechanical operation (file read, // arithmetic, well-defined CRUD); reverse-think on these is overhead // without insight. // // Default false. When false, no reverse-thinking pass is invoked // regardless of any hooks layered on; opt-in is per-tool. // // ELEVATED rationale: rule of two with RequiresCheckpoint -- both are // "self-describing tool flags consumed by hooks / handlers above core", // adding one alongside the existing one is cheaper than introducing a // new metadata category. // // 替代方案 (已否决): // A. 引擎级 ReverseThinkingGate 接口 (TODO L569 原方案 8 模块完整版) // -- 否决: 与 hooks / permission / validator / staging / RequiresCheckpoint // 5 套现有 gate 重叠, 业界零先例 (调研: Claude Agent SDK / OpenAI // Assistants / Vertex ADK / LangGraph / AutoGen / CrewAI / Semantic // Kernel / Cline 全部走 prompt-level + tool desc + 开发者自填策略). // B. 仅写 prompt 层引导, 不加字段 // -- 否决: prompt 易丢, 无法在 SDK 嵌入模式下程序级判定; 且 // Deliverable 持久化无锚点字段. // // RequiresReverseThinking 标记本工具调用前应走反向思维 pass: 产 counterfactual // .Deliverable (hidden_assumptions / failure_scenarios / verdict / // verdict_reason) 与调用一同持久化供审计和 evolve.Reflector 重放. // // 与 RequiresCheckpoint 边界: RequiresCheckpoint 问 "需要人确认吗?"; // RequiresReverseThinking 问 "agent 应不应该挑战自己的建议?". 两者可同时 // true (高风险调用值得两道); core 不规定顺序 -- 消费 hook 链 // (例如 platform/common/safetychain) 决定 reverse-think 在 checkpoint // 之前 / 之后 / 替代. // // 适用场景 (非平凡设计空间, 不是所有不可逆操作): // - 工具在多个候选策略中选择 (路由规划 / 承运商选择 / 策略参数) // - 工具编码失败模式不直观的决策 (账单匹配 / 反欺诈判定) // - 工具调下游 schema 可能漂移的系统 (三方 API / 合作方集成) // // 不适用: 纯机械操作 (文件读 / 算术 / 良定义 CRUD); 反向思维只是开销 // 没有洞察. // // 默认 false. false 时无论上层挂多少 hook 都不触发反向思维; 按工具 opt-in. // // 升华理由 (ELEVATED): 与 RequiresCheckpoint 走 rule of two -- 两者都是 // "core 之上 hook / handler 消费的自描述工具 flag", 在已有字段旁加同源字段 // 比新增 metadata 类别更便宜. RequiresReverseThinking bool } // MetadataProvider 是工具的可选接口. // 实现此接口的工具可以声明并发安全性等元数据. // // Shape: pull. Engine reads Metadata() at orchestration decisions // (can run in parallel? is destructive? requires checkpoint?). // // 形态: 调取 (pull). 引擎在编排决策时读 Metadata() (能否并发? 是否破坏性? // 要不要 checkpoint?). type MetadataProvider interface { Metadata() Metadata } // Result 是工具执行的结果. type Result struct { // Output 是工具的文本输出. Output string // IsError 标记输出是否为错误信息. IsError bool // Data 是结构化数据输出(可选). // 用于工具间传递结构化信息. Data any // UndoInfo 撤销信息(由 Reversible 工具填充). // 升华改进(ELEVATED): 撤销信息嵌入 Result 而非单独返回-- // 这样编排器在收集工具结果时自然就拿到了撤销信息,不需要额外调用. // 替代方案:在 Orchestrator 中单独调用 GenerateUndo(多一次调用,时序更复杂). UndoInfo *UndoInfo } // ProgressFunc 是进度回调函数类型. type ProgressFunc func(progress float64, detail string) // GetMetadata 安全获取工具的元数据. // 如果工具未实现 MetadataProvider,返回保守默认值. func GetMetadata(t Tool) Metadata { if mp, ok := t.(MetadataProvider); ok { return mp.Metadata() } // 保守默认值:假设不可并发,非只读,非破坏性 return Metadata{ ConcurrencySafe: false, ReadOnly: false, Destructive: false, } } // ToolPolicy declares which tools the engine should register. Replaces // the legacy `cfg.Tools []string` (allowlist-by-name) + `cfg.ExtraTools // []Tool` (always-additive on top of builtins) pair, which together had // no way to express "only this one tool, no builtins" — see ADR-0005 // § Bug A + B. // // ToolPolicy 声明引擎该注册哪些工具. 替代旧 `cfg.Tools []string` // (按名字白名单) + `cfg.ExtraTools []Tool` (在 builtin 之上始终叠加) 这对 // 字段 — 二者合起来无法表达 "只要这一个工具不要 builtin" — 见 ADR-0005 // § Bug A + B. // // Three constructors cover the design space: // // None() — empty registry; the model gets zero tools. // Allowlist(t...) — exactly these tools, in this order; no builtins // auto-injected. Caller imports core/pkg/tools/builtin // to get individual builtins, or core/extra/preset-coding // AllBuiltins helper (C12+) for the cc 23-tool slice. // (future) PolicyFunc — programmatic decision; reserved for cases where // the toolset depends on Run-time context. Not in C6. // // 三个构造函数覆盖设计空间: // // None() — 空注册表; 模型零工具. // Allowlist(t...) — 仅这些工具按此顺序, 不自动注入 builtin. caller 通过 // core/pkg/tools/builtin 拿单个 builtin, 或 (C12+) core/extra/preset-coding // 的 AllBuiltins helper 拿 cc 23 工具切片. // (未来) PolicyFunc — 程序化决策; 给"toolset 依赖 Run-time 上下文"的 // 场景预留. C6 不做. // // Aligns with Go database/sql (driver must Register explicitly, no default // driver) and Express middleware (no default body parser, opt in body // parser/cors/etc.). LangChain core does the same: BaseChatModel has no // default tools=. // // 对齐 Go database/sql (driver 必须显式 Register, 无默认 driver) 和 Express // middleware (无默认 body parser, body parser/cors 等显式开启). LangChain // core 同样: BaseChatModel 无默认 tools=. // // See ADR-0005 § 1.2 for the survey and § 2.3 for the design rationale. // // 业界对照见 ADR-0005 § 1.2, 设计依据见 § 2.3. type ToolPolicy interface { // Resolve returns the tools the engine should register, in the order // the model should see them. Empty slice or nil = no tools. // // Resolve 返回 engine 该注册的工具, 按模型看到的顺序. 空切片或 nil = 无工具. Resolve() []Tool } // None returns a ToolPolicy that resolves to an empty tool list. // // None 返回空工具列表的 ToolPolicy. func None() ToolPolicy { return noneTools{} } type noneTools struct{} func (noneTools) Resolve() []Tool { return nil } // Allowlist returns a ToolPolicy that resolves to exactly the given tools, // in the given order. Pass zero arguments for the same effect as None(). // // Allowlist 返回的 ToolPolicy 解析为给定工具按给定顺序. 零参数等价 None(). // // CLEVER: order matters — the model sees tools in this order in the // system prompt's tool list (when it lives there) and in the API tools // field. Some models bias toward earlier-listed tools. // // CLEVER: 顺序很关键 — 模型在 system prompt 的工具列表 (放那时) 和 API // tools 字段中按此顺序看到工具. 某些模型偏好排前面的工具. func Allowlist(t ...Tool) ToolPolicy { cp := make([]Tool, len(t)) copy(cp, t) return allowlistTools{tools: cp} } type allowlistTools struct { tools []Tool } func (a allowlistTools) Resolve() []Tool { return a.tools } // SocketAware is an optional interface implemented by tools that need the // engine to wire UDS (Unix Domain Socket) paths — e.g. BashTool receives // session/plan socket paths via SetSessionSockPath / SetPlanSockPath. // Engine code locates implementors by iterating the tool registry and // type-asserting, instead of hardcoding the tool name "Bash". // // SocketAware 是可选接口, 由需要引擎接 UDS (Unix Domain Socket) 路径的工具 // 实现 — 例如 BashTool 通过 SetSessionSockPath / SetPlanSockPath 接收 // session/plan socket 路径. engine 代码通过遍历工具注册表 + 类型断言定位 // 实现者, 不再硬编工具名 "Bash". // // See ADR-0005 § Bug Q. Pre-ADR-0005 the engine called // `e.tools.Get("Bash")` then type-asserted *builtin.BashTool. That // blocked any cfg.Toolset configuration like "I want only my custom tool, // no Bash" — the UDS init paths would silently no-op or panic depending // on assertion result. SocketAware decouples the requirement from the // tool name string. // // ADR-0005 § Bug Q. ADR-0005 之前 engine 调 `e.tools.Get("Bash")` 然后 // 类型断言 *builtin.BashTool. 这阻塞了 "我只要自定义工具不要 Bash" 这种 // cfg.Toolset 配置 — UDS init 路径会因断言结果静默 no-op 或 panic. // SocketAware 把"需求"从"工具名字符串"解耦. type SocketAware interface { // SetSessionSockPath sets the path to the session UDS socket. Called by // engine when the Inbox UDS server starts. // 设 session UDS socket 路径. engine 启动 Inbox UDS server 时调用. SetSessionSockPath(path string) // SetPlanSockPath sets the path to the plan-queue UDS socket. Called by // engine when PlanCommandServer starts. // 设 plan-queue UDS socket 路径. engine 启动 PlanCommandServer 时调用. SetPlanSockPath(path string) } // MessageIDAware is an optional interface implemented by tools that key // their per-turn side effects (notably file-history snapshots) by the // current turn id. The engine threads the SAME id the OperationLog records // (turnMessageID, "turn-N") at the start of every turn, so a later // Engine.Rollback(id) addresses the file snapshot AND the operation-log // entry under one consistent key. Without this wiring the snapshot key // stays the zero value "" and rollback silently restores nothing. // // Engine code locates implementors by iterating the tool registry and // type-asserting (mirrors SocketAware), so the wiring is decoupled from the // tool names "Edit" / "Write" (ADR-0005 § Bug Q discipline). // // MessageIDAware 是可选接口, 由 "按当前轮次 id 给每轮副作用 (主要是 // file-history 快照) 打键" 的工具实现. 引擎在每轮开始把与 OperationLog // 记录相同的 id (turnMessageID, "turn-N") 传入, 使随后的 Engine.Rollback(id) // 用同一个键既找到文件快照又找到操作日志条目. 不接这条线时快照键停留在 // 零值 "" -> 回滚静默什么都不还原. engine 通过遍历注册表 + 类型断言定位 // 实现者 (对位 SocketAware), 把接线从工具名 "Edit" / "Write" 解耦 // (ADR-0005 § Bug Q 纪律). type MessageIDAware interface { // SetMessageID sets the current turn id used to key per-turn side // effects. Called by the engine at the start of every turn. // // LEGACY / fallback only: storing the turn id on a SHARED tool instance // races when one engine runs concurrent Runs (e.g. platform serves // /agent/run + the plan queue on one engine), because each turn's // SetMessageID WRITE overlaps another Run's Execute READ of the same field // -- a data race that also corrupts file-history snapshot keys. The engine // now threads the turn id through the per-call context (WithMessageID); a // tool should prefer MessageIDFromContext(ctx) and treat the field set here // as a fallback for direct callers that do not thread a context. Implementing // this interface stays optional and backward compatible. // // 仅 LEGACY / fallback: 把轮次 id 存在 SHARED 工具实例上, 当一个 engine 跑并发 // Run 时会 race (如平台在一个 engine 上同时服务 /agent/run + 计划队列), 因为每轮 // SetMessageID 的 WRITE 与另一个 Run 的 Execute 对同一字段的 READ 重叠 -- 既是 // data race 又破坏 file-history 快照键. engine 现在经 per-call context 透传轮次 // id (WithMessageID); 工具应优先 MessageIDFromContext(ctx), 把这里设的字段当作 // 不透传 context 的直接调用者的 fallback. 实现本接口仍是可选且向后兼容. SetMessageID(id string) } // messageIDKey is the private context key carrying the current turn id (the // "turn-N" value the engine keys file-history snapshots + the OperationLog by). // Private struct type so external code cannot forge or collide with it via // context.WithValue (mirrors the engine's eventEmitterKey pattern). // // messageIDKey 是携带当前轮次 id 的私有 context key (engine 用 "turn-N" 给 // file-history 快照 + OperationLog 打键的那个值). 私有 struct 类型, 外部代码无法经 // context.WithValue 伪造或撞 key (镜像 engine 的 eventEmitterKey 模式). type messageIDKey struct{} // WithMessageID derives a context carrying the current turn id for tools that // key per-turn side effects (file-history snapshots). The engine sets it once // per turn on the context it passes to tool execution, so concurrent Runs on one // engine each carry their OWN id with no shared-instance race -- the race-free, // correct replacement for the shared SetMessageID field. An empty id is a no-op // (the consumer falls back to its SetMessageID field, or the zero key ""). // // WithMessageID 派生一个携带当前轮次 id 的 context, 供按轮次打键副作用 // (file-history 快照) 的工具用. engine 每轮在传给工具执行的 context 上设一次, 故 // 一个 engine 上的并发 Run 各带自己的 id, 无共享实例 race -- 是共享 SetMessageID // 字段的 race-free 且正确的替代. 空 id 为 no-op (消费者回落到 SetMessageID 字段, 或 // 零值键 ""). func WithMessageID(ctx context.Context, id string) context.Context { if id == "" { return ctx } return context.WithValue(ctx, messageIDKey{}, id) } // MessageIDFromContext returns the turn id set by WithMessageID, or "" if none. // Tools implementing MessageIDAware should prefer this over their stored field. // // MessageIDFromContext 返回 WithMessageID 设的轮次 id, 未设则 "". 实现 MessageIDAware // 的工具应优先用它而非存储的字段. func MessageIDFromContext(ctx context.Context) string { id, _ := ctx.Value(messageIDKey{}).(string) return id }