|
| 1 | +# SubAgent 多目录加载支持 |
| 2 | + |
| 3 | +**Date:** 2025-12-29 |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +当前项目中已经实现了 SubAgent 系统(`src/agent/`),但 SubAgent 定义只能通过代码注册(内置或插件)。 |
| 8 | + |
| 9 | +**当前代码结构:** |
| 10 | +- `src/agent/agentManager.ts` - 核心 AgentManager 类,负责 agent 注册和执行 |
| 11 | +- `src/agent/types.ts` - 已包含 `AgentSource` 枚举和 `AgentDefinition` 接口 |
| 12 | +- `src/agent/builtin/` - 内置 agent(如 explore),使用 `AgentSource.BuiltIn` |
| 13 | +- `AgentManager` 构造函数中调用 `registerBuiltinAgents()` 注册内置 agent |
| 14 | + |
| 15 | +项目中存在 `skill.ts`,它实现了从多个目录加载 skill 配置文件的能力,支持项目级和用户级两个层次,以及 `.claude` 和 `.neovate` 两种配置目录。 |
| 16 | + |
| 17 | +用户希望为 SubAgent 添加类似的文件加载能力,使 SubAgent 可以通过配置文件定义,而不仅仅通过代码注册。这将提高 SubAgent 的可配置性和可扩展性,让用户能够更方便地创建和管理自定义的 SubAgent。 |
| 18 | + |
| 19 | +## Discussion |
| 20 | + |
| 21 | +### 核心目标确认 |
| 22 | +经过讨论,明确了实现目标是:**参考 `skill.ts` 为 SubAgent 添加多目录支持**,而不是创建独立的文件夹管理系统或仅实现单一配置加载。 |
| 23 | + |
| 24 | +### 文件结构选择 |
| 25 | +讨论了三种文件结构方案: |
| 26 | +1. **文件夹 + 固定名称文件**(如 skill 的 `SKILL.md`) |
| 27 | +2. **直接 .md 文件**(选定) |
| 28 | +3. **混合模式** |
| 29 | + |
| 30 | +最终选择了**直接 .md 文件**的方式,即在 `agents/` 目录下直接放置 `.md` 文件,文件名即为 agent 名称。这种方式比 skill 的文件夹方式更简洁,适合 SubAgent 的使用场景。 |
| 31 | + |
| 32 | +### 目录优先级策略 |
| 33 | +确定使用与 skill 一致的四层目录优先级: |
| 34 | +``` |
| 35 | +Global (~/.neovate/agents/) |
| 36 | + ↓ |
| 37 | +GlobalClaude (~/.claude/agents/) |
| 38 | + ↓ |
| 39 | +Project (.neovate/agents/) |
| 40 | + ↓ |
| 41 | +ProjectClaude (.claude/agents/) ← 最高优先级 |
| 42 | +``` |
| 43 | + |
| 44 | +同名 SubAgent 时,后加载的覆盖先加载的,因此项目级别的配置会覆盖全局配置。 |
| 45 | + |
| 46 | +### 实现方案比较 |
| 47 | +讨论了三种实现方案: |
| 48 | + |
| 49 | +**方案一:完全参考 skill.ts** |
| 50 | +- 创建新的 `SubAgentManager` 类 |
| 51 | +- 几乎完全复制 `SkillManager` 的逻辑 |
| 52 | +- 优点:实现简单,风险低,与 skill 保持一致 |
| 53 | +- 缺点:增加代码量,需要与现有 `AgentManager` 整合 |
| 54 | + |
| 55 | +**方案二:扩展现有 AgentManager**(选定) |
| 56 | +- 在现有 `AgentManager` 类中添加文件加载功能 |
| 57 | +- 优点:代码更集中,所有 agent 统一管理 |
| 58 | +- 缺点:`AgentManager` 职责增加 |
| 59 | + |
| 60 | +**方案三:混合模式 - Manager 分离 + Loader 复用** |
| 61 | +- 创建独立的 `SubAgentManager` 但抽取通用的文件加载逻辑 |
| 62 | +- 优点:代码复用性最高 |
| 63 | +- 缺点:工作量大,需要重构现有代码 |
| 64 | + |
| 65 | +最终选择了**方案二**,在现有 `AgentManager` 中添加文件加载能力,保持代码集中和 API 一致性。 |
| 66 | + |
| 67 | +## Approach |
| 68 | + |
| 69 | +通过扩展现有的 `AgentManager` 类实现多目录 SubAgent 配置文件加载: |
| 70 | + |
| 71 | +1. **扩展类型定义**:在 `AgentDefinition` 中增加文件来源的 source 类型(`project-claude`、`project`、`global-claude`、`global`) |
| 72 | + |
| 73 | +2. **添加文件加载方法**: |
| 74 | + - 在构造函数中自动调用 `loadAgentsFromFiles()` |
| 75 | + - 遍历四个配置目录,按优先级顺序加载 |
| 76 | + - 读取 `.md` 文件,使用 `safeFrontMatter` 解析 YAML 前置内容 |
| 77 | + - 验证必需字段(name、description)和可选字段(tools、model) |
| 78 | + |
| 79 | +3. **保持向后兼容**: |
| 80 | + - 不改变现有的内置和插件 agent 注册机制 |
| 81 | + - `executeTask()` 等现有 API 保持不变 |
| 82 | + - 自动整合文件定义的 agents 到统一的 agents Map 中 |
| 83 | + |
| 84 | +4. **完善错误处理**: |
| 85 | + - 添加 `errors` 数组记录加载过程中的错误 |
| 86 | + - 提供 `getErrors()` 方法供外部查询 |
| 87 | + - 单个文件加载失败不影响其他文件 |
| 88 | + |
| 89 | +## Architecture |
| 90 | + |
| 91 | +### 1. 类型定义扩展 |
| 92 | + |
| 93 | +在 `src/agent/types.ts` 中: |
| 94 | + |
| 95 | +```typescript |
| 96 | +// AgentSource 枚举已存在 |
| 97 | +export enum AgentSource { |
| 98 | + BuiltIn = 'built-in', |
| 99 | + Plugin = 'plugin', |
| 100 | + User = 'user', |
| 101 | + ProjectClaude = 'project-claude', |
| 102 | + Project = 'project', |
| 103 | + GlobalClaude = 'global-claude', |
| 104 | + Global = 'global', |
| 105 | +} |
| 106 | + |
| 107 | +// AgentDefinition 已使用 AgentSource 类型,需添加 path 字段 |
| 108 | +export interface AgentDefinition { |
| 109 | + agentType: string; |
| 110 | + whenToUse: string; |
| 111 | + systemPrompt: string; |
| 112 | + model: string; |
| 113 | + source: AgentSource; // 已存在 |
| 114 | + tools?: string[]; |
| 115 | + disallowedTools?: string[]; |
| 116 | + forkContext?: boolean; |
| 117 | + color?: string; |
| 118 | + path?: string; // 新增:记录文件路径 |
| 119 | +} |
| 120 | + |
| 121 | +// 新增接口 |
| 122 | +export interface AgentLoadError { |
| 123 | + path: string; |
| 124 | + message: string; |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +### 2. AgentManager 类扩展 |
| 129 | + |
| 130 | +在 `src/agent/agentManager.ts` 中添加: |
| 131 | + |
| 132 | +**新增属性:** |
| 133 | +```typescript |
| 134 | +private errors: AgentLoadError[] = []; |
| 135 | +``` |
| 136 | + |
| 137 | +**新增方法:** |
| 138 | +```typescript |
| 139 | +// 主加载方法 |
| 140 | +async loadAgentsFromFiles(): Promise<void> |
| 141 | + |
| 142 | +// 目录扫描 |
| 143 | +private loadAgentsFromDirectory(dir: string, source: AgentSource): void |
| 144 | + |
| 145 | +// 单文件加载 |
| 146 | +private loadAgentFile(filePath: string, source: AgentSource): void |
| 147 | + |
| 148 | +// 文件解析 |
| 149 | +private parseAgentFile(content: string, filePath: string): |
| 150 | + Omit<AgentDefinition, 'source' | 'path'> | null |
| 151 | + |
| 152 | +// 错误查询 |
| 153 | +getErrors(): AgentLoadError[] |
| 154 | +``` |
| 155 | + |
| 156 | +### 3. 加载流程 |
| 157 | + |
| 158 | +``` |
| 159 | +应用启动 |
| 160 | + ↓ |
| 161 | +AgentManager 构造函数 |
| 162 | + ↓ |
| 163 | +registerBuiltinAgents() |
| 164 | + ↓ |
| 165 | +loadAgentsFromFiles() |
| 166 | + ↓ |
| 167 | +遍历 4 个目录(Global → GlobalClaude → Project → ProjectClaude) |
| 168 | + ↓ |
| 169 | +每个目录: |
| 170 | + - 检查目录是否存在 |
| 171 | + - 读取所有 .md 文件 |
| 172 | + - 解析 YAML frontmatter |
| 173 | + - 验证必需字段 |
| 174 | + - 存入 agents Map(同名覆盖) |
| 175 | + ↓ |
| 176 | +加载完成 |
| 177 | +``` |
| 178 | + |
| 179 | +### 4. 文件格式 |
| 180 | + |
| 181 | +**YAML 前置内容字段:** |
| 182 | +- `name`(必需):agent 标识符,小写字母+连字符,最大 64 字符,单行 |
| 183 | +- `description`(必需):自然语言描述,最大 1024 字符,单行 |
| 184 | +- `tools`(可选):逗号分隔的工具列表,省略则继承所有工具 |
| 185 | +- `disallowedTools`(可选):逗号分隔的禁用工具列表 |
| 186 | +- `model`(可选):模型别名或 'inherit' |
| 187 | +- `forkContext`(可选):布尔值,是否继承上下文 |
| 188 | +- `color`(可选):agent 显示颜色 |
| 189 | + |
| 190 | +**Body 部分:** 作为 `systemPrompt` 使用(必需,不能为空) |
| 191 | + |
| 192 | +**文件示例:** |
| 193 | +```markdown |
| 194 | +--- |
| 195 | +name: code-reviewer |
| 196 | +description: Reviews code changes and provides feedback on code quality |
| 197 | +tools: read, grep, bash |
| 198 | +disallowedTools: write, edit |
| 199 | +model: sonnet |
| 200 | +forkContext: false |
| 201 | +color: purple |
| 202 | +--- |
| 203 | + |
| 204 | +You are a code review assistant. Your role is to: |
| 205 | + |
| 206 | +1. Analyze code changes carefully |
| 207 | +2. Identify potential bugs and security issues |
| 208 | +3. Suggest improvements following best practices |
| 209 | +``` |
| 210 | + |
| 211 | +### 5. 验证规则 |
| 212 | + |
| 213 | +- `name` 和 `description` 必需且为单行 |
| 214 | +- `name` 长度 ≤ 64 字符 |
| 215 | +- `description` 长度 ≤ 1024 字符 |
| 216 | +- `systemPrompt`(body)不能为空 |
| 217 | +- 只处理 `.md` 文件 |
| 218 | +- YAML 解析失败时记录错误并跳过 |
| 219 | + |
| 220 | +### 6. 错误处理 |
| 221 | + |
| 222 | +**加载阶段:** |
| 223 | +- 目录不存在:跳过,不记录错误 |
| 224 | +- 文件读取/解析失败:记录到 errors 数组,继续加载其他文件 |
| 225 | +- 字段验证失败:记录具体错误,跳过该文件 |
| 226 | + |
| 227 | +**运行时:** |
| 228 | +- agent 不存在:抛出异常,提示可用的 agent 类型 |
| 229 | +- tools 配置错误:在执行前验证并报错 |
| 230 | + |
| 231 | +### 7. 工具过滤 |
| 232 | + |
| 233 | +如果 agent 定义了 `tools` 字段: |
| 234 | +```typescript |
| 235 | +const allowedTools = definition.tools |
| 236 | + ? context.tools.filter(t => definition.tools!.includes(t.name)) |
| 237 | + : context.tools; |
| 238 | +``` |
| 239 | + |
| 240 | +### 8. 集成点 |
| 241 | + |
| 242 | +- **Context**:使用 `context.paths.globalConfigDir` 和 `context.paths.projectConfigDir` |
| 243 | +- **工具**:`src/tools/task.ts` 无需修改,自动支持文件定义的 agents |
| 244 | +- **系统提示**:`getAgentDescriptions()` 自动包含文件定义的 agents |
| 245 | + |
| 246 | +### 9. 测试策略 |
| 247 | + |
| 248 | +创建 `src/agent/agentManager.test.ts`,覆盖: |
| 249 | +- 有效文件加载 |
| 250 | +- 必需字段验证 |
| 251 | +- 长度限制验证 |
| 252 | +- 同名覆盖逻辑 |
| 253 | +- tools/model 字段解析 |
| 254 | +- 错误处理(目录不存在、YAML 解析失败等) |
| 255 | +- 非 .md 文件跳过 |
| 256 | + |
| 257 | +### 10. 向后兼容性 |
| 258 | + |
| 259 | +- 内置/插件 agent 注册机制不变 |
| 260 | +- 所有现有 API 保持不变 |
| 261 | +- 新增 `getErrors()` 方法(可选调用) |
| 262 | +- 文件加载失败不影响已注册的 agents |
0 commit comments