// 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
}