Skip to content

Commit a29e2fe

Browse files
authored
feat: add multi-directory SubAgent file loading support (#593)
* feat: add multi-directory SubAgent file loading support * refactor: use TOOL_NAMES constants for tool mapping
1 parent c10a567 commit a29e2fe

File tree

4 files changed

+512
-0
lines changed

4 files changed

+512
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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

Comments
 (0)