package engine // agent_def.go 实现 Agent 类型注册表和工具集解析. // // 模块定位: // // Agent 类型系统 -- 描述"一种 Agent 该有什么能力", // 而非"某个 Agent 实例当前在干什么". // 类比 Kubernetes 的 Deployment 定义(模板),而非 Pod(运行实例). // // 核心设计决策: // 1. 四层工具过滤(从宽到严): // 父工具集 → AllowedTools 交集(MCP 前缀自动通过) // → Background 限制(后台 agent 附加收窄) // → DisallowedTools + globalDisallowed 差集 // 精妙之处(CLEVER): 层次化过滤比单一黑/白名单更灵活-- // 新增工具时只需关心白名单定义,禁用逻辑不受影响. // 替代方案:单一黑名单(新工具默认可用,可能意外暴露危险工具). // 2. MCP 工具(mcp__ 前缀)自动通过 AllowedTools 交集-- // MCP 工具名由服务器动态注册(如 mcp__github__create_issue), // 不可能在静态 AllowedTools 里预先列出. // 精妙之处(CLEVER): 前缀识别解决"静态白名单 vs 动态服务"的矛盾-- // Explore/Plan 等限制型 agent 不会误屏蔽所有 MCP 工具. // 替代方案:要求 AllowedTools 里用通配符(语法复杂,用户易出错). // 3. globalDisallowed 默认包含危险工具集--SDK 层可通过 SetGlobalDisallowed 覆盖. // 精妙之处(CLEVER): 覆盖而非叠加--SDK 场景可能需要完全不同的安全边界. // 替代方案:永不可覆盖的全局禁用(无法适应不同消费层的需求). // 4. Register 重名覆盖(非报错):允许热更新定义,方便插件/运行时重载. // 替代方案:重名报错(强制唯一性,但阻碍运行时更新). import ( "fmt" "sort" "strings" "sync" "git.flytoex.net/yuanwei/flyto-agent/pkg/config" ) // mcpToolPrefix 是所有 MCP 工具名的统一前缀(mcp____). // // 升华改进(ELEVATED): 早期实现用 isMcpTool() 函数判断前缀. // 我们提取为命名常量,消除魔法字符串,让过滤逻辑的意图一目了然. // 替代方案:在 resolveAgentToolset 里内联 "mcp__" 字符串(散落各处,难以修改). const mcpToolPrefix = "mcp__" // DefaultGlobalDisallowedTools 是生产环境中所有子 Agent 默认禁用的工具集合. // // 升华改进(ELEVATED): 早期实现的 ALL_AGENT_DISALLOWED_TOOLS // 包含 7 个工具,硬编码在 filterToolsForAgent() 函数内. // 我们提取为可覆盖的包级变量--NewAgentRegistry() 用它初始化默认值, // SDK 消费层可通过 SetGlobalDisallowed() 替换为完全不同的集合. // 跨行业影响:测试环境可传入空切片,医疗合规场景可追加私有工具. // 替代方案:<硬编码在 NewAgentRegistry() 内部> - 否决:测试/SDK 无法覆盖. var DefaultGlobalDisallowedTools = []string{ // 防止无限递归:子 agent 不能再启动子 agent(默认禁止,AgentRegistry 级别控制) "Agent", // 计划模式状态机:子 agent 不能改变父 agent 的计划模式状态 "ExitPlanMode", "EnterPlanMode", // 用户交互:子 agent 不应直接向用户提问(父 agent 负责与用户沟通) // 注意:AskUserQuestion 工具尚未在引擎中实现,此处预留防止未来意外暴露 "AskUserQuestion", // 任务生命周期:以下工具是内部会话级别工具,子 agent 不应调用 // TaskOutput:子 agent 完成应通过返回值传达结果,而非推送输出 "TaskOutput", // TaskStop:停止父 agent 的任务是越权行为 "TaskStop", } // AgentDefinition 定义一种 Agent 类型的行为约束和资源配置. // // 升华改进(ELEVATED): 不只是"工具白名单",而是完整的 Agent 能力档案. // 包含使用指引(WhenToUse),让 AI 能自主选择合适的 Agent 类型. // 跨行业扩展:金融场景可定义 "Compliance" 类型(只允许查询工具), // // 医疗场景可定义 "Diagnosis" 类型(禁用所有写操作). type AgentDefinition struct { // AgentType 唯一标识符,如 "Explore","Plan" AgentType string // Description 一句话描述,展示给用户/模型 Description string // WhenToUse 何时使用的指引文案,辅助 AI 做类型选择 WhenToUse string // AllowedTools 允许使用的工具白名单(nil = 继承父代理所有工具). // MCP 工具(mcp__ 前缀)自动通过此过滤,无需在此列出. AllowedTools []string // DisallowedTools 额外禁用的工具(叠加在全局禁用之上) DisallowedTools []string // Model 使用的模型("" = 继承父代理模型) Model string // MaxTurns 最大轮数(0 = 默认 10) MaxTurns int // Background 是否默认后台运行. // 当 Background=true 且 BackgroundAllowedTools 非空时, // 在 AllowedTools 基础上额外收窄到 BackgroundAllowedTools. Background bool // BackgroundAllowedTools 后台运行时的附加工具白名单. // // 升华改进(ELEVATED): 早期实现 用全局常量 ASYNC_AGENT_ALLOWED_TOOLS(约16个工具), // 所有 isAsync=true 的 agent 都受此约束,无法针对不同 agent 类型定制. // 我们改为 per-agent 配置--每种 Agent 类型声明自己的后台工具集, // 仓储场景可以把 mcp__wms__* 加进去,而默认 Verification agent 只需文件读工具. // MCP 工具(mcp__ 前缀)仍然自动通过此过滤. // nil = Background 模式不附加额外限制(仍受 AllowedTools 约束). // 替代方案:<全局 ASYNC_AGENT_ALLOWED_TOOLS 常量> - 否决:扩展性差, // 增加新 agent 类型时必须修改全局常量. BackgroundAllowedTools []string // AllowedSubAgentTypes 限制此 Agent 可以 spawn 的子 Agent 类型. // // 升华改进(ELEVATED): 早期实现 resolveAgentTools() 第191-195行用 allowedAgentTypes // 元数据约束 Agent 工具只能 spawn 特定类型,但通过字符串解析("Agent(Explore)")实现. // 我们改为结构化字段--类型安全,易于序列化,不需要解析器. // 跨行业影响:Explore 类型只能 spawn Plan/Verification 子 agent(禁止 general-purpose), // 防止只读 agent 通过 spawn 全能 agent 来绕过工具限制. // nil/空 = 无限制,可 spawn 任何已注册的 AgentType. // 替代方案:<字符串语法 Agent(Explore, Plan)> - 否决:需要解析器,容易写错. AllowedSubAgentTypes []string } // AgentRegistry 是 Agent 类型注册表. // // 线程安全:所有操作通过 sync.RWMutex 保护. // 升华改进(ELEVATED): 注册表是全局知识库,工具集解析是运行时决策-- // 两个关注点在同一结构体内,但通过 resolveAgentToolset 明确分离. // 替代方案:注册表只存储定义,工具集解析独立为包级函数(更分散,不利于封装 globalDisallowed). type AgentRegistry struct { mu sync.RWMutex defs map[string]*AgentDefinition // globalDisallowed 是所有子代理都禁用的工具集合. // 默认只有 "Agent"(防止递归). // SDK 可通过 SetGlobalDisallowed 覆盖. // // 精妙之处(CLEVER): globalDisallowed 用 map[string]struct{} 而非 []string-- // O(1) 查找 vs O(n) 线性扫描,工具数量大时差异明显. // 替代方案:[]string(更直观,但查找性能差). globalDisallowed map[string]struct{} } // NewAgentRegistry 创建 Agent 类型注册表. // // 默认使用 DefaultGlobalDisallowedTools 初始化全局禁用集合-- // 包含递归防护,计划模式保护,用户交互保护等安全工具集. // // 升华改进(ELEVATED): 早期实现 ALL_AGENT_DISALLOWED_TOOLS 硬编码在 filterToolsForAgent() 内, // 无法被 SDK 消费层覆盖.我们通过 DefaultGlobalDisallowedTools 变量 + SetGlobalDisallowed() // 实现"默认安全 + 可覆盖": // - 测试环境:SetGlobalDisallowed([]string{}) 清空,完全开放 // - 生产强化:SetGlobalDisallowed(append(DefaultGlobalDisallowedTools, "MyPrivateTool")) // // 替代方案:<硬编码 map 字面量> - 否决:无法表达 DefaultGlobalDisallowedTools 的来源意图. func NewAgentRegistry() *AgentRegistry { r := &AgentRegistry{ defs: make(map[string]*AgentDefinition), globalDisallowed: make(map[string]struct{}, len(DefaultGlobalDisallowedTools)), } for _, name := range DefaultGlobalDisallowedTools { r.globalDisallowed[name] = struct{}{} } return r } // Register 注册一个 Agent 类型定义. // 重名时覆盖旧定义(允许热更新/插件重载). func (r *AgentRegistry) Register(def *AgentDefinition) error { if def == nil { return fmt.Errorf("agent_registry: definition must not be nil") } if def.AgentType == "" { return fmt.Errorf("agent_registry: AgentType must not be empty") } r.mu.Lock() defer r.mu.Unlock() r.defs[def.AgentType] = def return nil } // Get 按类型名称查找 Agent 定义. func (r *AgentRegistry) Get(agentType string) (*AgentDefinition, bool) { r.mu.RLock() defer r.mu.RUnlock() def, ok := r.defs[agentType] return def, ok } // List 返回所有已注册的 Agent 定义,按 AgentType 字母顺序排序. // // 精妙之处(CLEVER): 排序确保输出稳定--测试断言,UI 展示都不会因 map 随机遍历而抖动. // 替代方案:直接返回 map 值(顺序随机,测试不稳定). func (r *AgentRegistry) List() []*AgentDefinition { r.mu.RLock() defer r.mu.RUnlock() result := make([]*AgentDefinition, 0, len(r.defs)) for _, def := range r.defs { result = append(result, def) } sort.Slice(result, func(i, j int) bool { return result[i].AgentType < result[j].AgentType }) return result } // SetGlobalDisallowed 覆盖全局禁用工具列表. // SDK 消费层可通过此方法定制安全边界. // // 升华改进(ELEVATED): "覆盖"而非"追加"--SDK 场景可能需要完全重置禁用列表. // 例如:测试环境可传入空切片(完全开放),生产环境可传入更严格的列表. // 替代方案:追加模式(无法删除已有禁用项). func (r *AgentRegistry) SetGlobalDisallowed(toolNames []string) { r.mu.Lock() defer r.mu.Unlock() newSet := make(map[string]struct{}, len(toolNames)) for _, name := range toolNames { newSet[name] = struct{}{} } r.globalDisallowed = newSet } // resolveAgentToolset 计算 Agent 类型最终可用的工具集合. // // 四层过滤(从宽到严): // 1. 从父代理工具集出发 // 2. 如果 def.AllowedTools 非空,取交集(白名单);MCP 工具自动通过此层 // 3. 如果 def.Background=true 且 BackgroundAllowedTools 非空,取交集(附加收窄);MCP 工具自动通过 // 4. 移除 def.DisallowedTools + globalDisallowed // // 精妙之处(CLEVER): 四层过滤的顺序是关键--先收窄(白名单),再排除(黑名单). // 如果先排除再收窄,白名单可能意外包含已被全局禁用的工具. // 替代方案:单层黑名单(无法表达"只允许特定工具"的约束). // // 精妙之处(CLEVER): MCP 工具(mcp__ 前缀)在白名单层自动通过. // MCP 工具名由服务器动态注册(格式 mcp____),不可预知. // 若不豁免,凡是设了 AllowedTools 的 agent 类型(如 Explore,Plan) // 都会屏蔽所有 MCP 工具,导致 MCP 完全不可用. // 豁免后 MCP 工具仍受 DisallowedTools + globalDisallowed 约束(第四层). // 替代方案:<要求在 AllowedTools 里用通配符> - 否决:通配符支持复杂,用户易忘写. // // 线程安全:调用者必须在持有读锁的情况下调用,或确保无并发写入. // 注意:此方法内部不加锁,以允许被加锁的方法复用. func (r *AgentRegistry) resolveAgentToolset(def *AgentDefinition, parentTools []string) []string { // 第一层:从父代理工具集出发 workingSet := make(map[string]struct{}, len(parentTools)) for _, t := range parentTools { workingSet[t] = struct{}{} } // 第二层:如果 AllowedTools 非空,取交集(白名单) // 精妙之处(CLEVER): MCP 工具跳过此层--strings.HasPrefix(t, mcpToolPrefix) 检查 // 让 mcp__github__search 这类动态名称不需要预先列在 AllowedTools 里. // 如果不跳过:Explore agent 的 AllowedTools=["Read","Grep","Glob"] // 会屏蔽所有 mcp__ 工具,即使有 MCP 服务器注册了只读查询工具也无法使用. if len(def.AllowedTools) > 0 { allowedSet := make(map[string]struct{}, len(def.AllowedTools)) for _, t := range def.AllowedTools { allowedSet[t] = struct{}{} } // 保留 workingSet ∩ allowedSet(MCP 工具无条件保留) for t := range workingSet { if strings.HasPrefix(t, mcpToolPrefix) { continue // MCP 工具自动通过,不参与交集 } if _, ok := allowedSet[t]; !ok { delete(workingSet, t) } } } // 第三层:后台运行附加收窄(仅当 Background=true 且 BackgroundAllowedTools 非空) // // 升华改进(ELEVATED): 早期实现 ASYNC_AGENT_ALLOWED_TOOLS 是全局常量(约16个工具), // 所有后台 agent 共享同一限制,无法为不同 agent 类型定制. // 我们改为 per-agent 的 BackgroundAllowedTools 字段-- // 仓储场景的 background agent 可以包含 mcp__wms__* 工具, // 而标准 Verification agent 只需文件读工具. // MCP 工具同样自动通过此层(与第二层一致). // 替代方案:<全局变量 DefaultBackgroundAllowedTools> // - 否决:破坏了 per-agent 定制能力,仓储/金融/医疗场景各有不同需求. if def.Background && len(def.BackgroundAllowedTools) > 0 { bgSet := make(map[string]struct{}, len(def.BackgroundAllowedTools)) for _, t := range def.BackgroundAllowedTools { bgSet[t] = struct{}{} } for t := range workingSet { if strings.HasPrefix(t, mcpToolPrefix) { continue // MCP 工具自动通过后台限制 } if _, ok := bgSet[t]; !ok { delete(workingSet, t) } } } // 第四层:移除 DisallowedTools + globalDisallowed(黑名单,优先级最高) // // 精妙之处(CLEVER): 黑名单在白名单之后应用--即使 MCP 工具通过了第二,三层豁免, // 如果它出现在 DisallowedTools 或 globalDisallowed 里,仍然被移除. // 这保证了安全工具集(如 "mcp__internal__delete_all")可以被精确禁用. for _, t := range def.DisallowedTools { delete(workingSet, t) } for t := range r.globalDisallowed { delete(workingSet, t) } // 转为有序切片(保证输出稳定) result := make([]string, 0, len(workingSet)) for t := range workingSet { result = append(result, t) } sort.Strings(result) return result } // ResolveToolset 公开版本的 resolveAgentToolset,供外部调用(如 agentExecutor). // // 线程安全:持有读锁. func (r *AgentRegistry) ResolveToolset(def *AgentDefinition, parentTools []string) []string { r.mu.RLock() defer r.mu.RUnlock() return r.resolveAgentToolset(def, parentTools) } // RegisterBuiltinAgents 注册 4 种内置 Agent 类型到注册表. // // 内置类型设计原则: // - general-purpose: 全能型,不限制工具(基准线) // - Explore: 只读探索,禁止写操作(安全探索代码库) // - Plan: 只读规划,禁止执行(只允许思考,不允许操作) // - Verification: 验证型,禁止写操作,默认后台(不干扰主流程) // // 升华改进(ELEVATED): 内置类型是"工厂默认配置"-- // 用户/SDK 可以 Register 同名定义覆盖,实现企业级定制. // 替代方案:内置类型硬编码为常量(无法覆盖,不支持定制). func RegisterBuiltinAgents(r *AgentRegistry) { // 1. general-purpose:全能,不限制工具 _ = r.Register(&AgentDefinition{ AgentType: "general-purpose", Description: "General-purpose agent with access to all available tools.", WhenToUse: "Use when the task requires a full range of capabilities.", // AllowedTools nil = 继承父代理所有工具 // DisallowedTools nil = 只受 globalDisallowed 约束 }) // 2. Explore:快速代码库探索,只读 // 精妙之处(CLEVER): 使用 RoleFast 模型(Haiku)--探索任务不需要高质量推理, // 用廉价快速模型可显著降低成本.一次完整探索任务节省约 80% 模型费用. // 替代方案:继承父代理模型(昂贵,浪费). // // P1 注记:若 config.DefaultRoles[config.RoleFast] 变化,此处自动跟随. // 不硬编码具体模型 ID,通过 config 包的角色系统间接引用. _ = r.Register(&AgentDefinition{ AgentType: "Explore", Description: "Fast agent specialized for exploring codebases.", WhenToUse: "Use when you need to quickly find files, search code, or answer questions about the codebase.", DisallowedTools: []string{"Edit", "Write", "NotebookEdit"}, // 使用 RoleFast 角色对应的模型(默认为 claude-haiku-4-5) // ELEVATED: 通过角色系统引用,避免硬编码模型 ID Model: config.DefaultRoles[config.RoleFast], Background: false, }) // 3. Plan:软件架构规划,只读不执行 _ = r.Register(&AgentDefinition{ AgentType: "Plan", Description: "Software architect agent for designing implementation plans.", WhenToUse: "Use when you need to plan the implementation strategy for a task.", DisallowedTools: []string{"Edit", "Write", "NotebookEdit", "Bash"}, Background: false, }) // 4. Verification:验证实现正确性,只读,默认后台 _ = r.Register(&AgentDefinition{ AgentType: "Verification", Description: "Agent for verifying correctness of implementations.", WhenToUse: "Use after implementation to verify the solution is correct.", DisallowedTools: []string{"Edit", "Write", "NotebookEdit"}, Background: true, // 默认后台运行,不阻塞主流程 }) }