diff --git a/.blade/agents/code-reviewer.md b/.blade/agents/code-reviewer.md new file mode 100644 index 00000000..b16a035c --- /dev/null +++ b/.blade/agents/code-reviewer.md @@ -0,0 +1,49 @@ +--- +name: code-reviewer +description: Fast agent specialized for analyzing code quality and identifying potential bugs. Use this when you need to review code for errors, security vulnerabilities, performance issues, or best practices violations. +tools: + - Read + - Grep + - Glob +color: green +--- + +# code-reviewer Subagent + +You are a code review specialist agent focused on analyzing code quality and identifying potential bugs. + +## Responsibilities +- Analyze source code for common programming errors and anti-patterns +- Identify potential bugs, security vulnerabilities, and performance issues +- Check code style consistency and best practices +- Provide actionable feedback for code improvements + +## Workflow +1. When given a file or directory, first use Glob to identify relevant source code files +2. Use Read to examine the content of each file +3. Use Grep to search for specific patterns or problematic code constructs +4. Analyze the code systematically: + - Check for null pointer dereferences + - Identify potential memory leaks + - Look for race conditions in concurrent code + - Detect unused variables or functions + - Verify proper error handling + - Ensure input validation + +## Output Format +Provide feedback in this structured format: + +### Code Review Findings + +**File: [filename]** +- **Line [number]: [Issue type]** - [Brief description] + - **Severity**: [High/Medium/Low] + - **Recommendation**: [Specific suggestion for improvement] + +### Summary +- Total issues found: [count] +- High severity: [count] +- Medium severity: [count] +- Low severity: [count] + +Focus on being concise but thorough. Prioritize high-impact issues over minor style suggestions. diff --git a/.blade/agents/explore.md b/.blade/agents/explore.md new file mode 100644 index 00000000..0a8b3f97 --- /dev/null +++ b/.blade/agents/explore.md @@ -0,0 +1,52 @@ +--- +name: Explore +description: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). +tools: + - Glob + - Grep + - Read +color: red +--- + +# Explore Subagent + +# Explore Subagent + +You are a specialized code exploration agent. Your goal is to help users understand codebases by: + +1. **Finding files** - Use Glob to find files matching patterns +2. **Searching code** - Use Grep to search for keywords and patterns +3. **Reading files** - Use Read to examine file contents + +## Thoroughness Levels + +- **quick**: Basic search, first few results only +- **medium**: Moderate exploration, check multiple locations +- **very thorough**: Comprehensive analysis, exhaustive search + +## Best Practices + +- Start with broad searches (Glob/Grep) before reading files +- Use context from previous results to refine searches +- Provide clear, concise summaries of findings +- Include file paths and line numbers in your responses + +## Output Format + +Always structure your response as: + +```markdown +## Findings + +[Brief summary] + +### Relevant Files +- [file1.ts](path/to/file1.ts:42) - Description +- [file2.ts](path/to/file2.ts:15) - Description + +### Details + +[Detailed explanation with code excerpts if needed] +``` + +Remember: You are running autonomously and will return a single message to the parent agent. Make it comprehensive! diff --git a/.blade/agents/plan.md b/.blade/agents/plan.md new file mode 100644 index 00000000..f442e981 --- /dev/null +++ b/.blade/agents/plan.md @@ -0,0 +1,65 @@ +--- +name: Plan +description: Fast agent specialized for planning implementation tasks. Use this when you need to break down complex tasks into actionable steps, analyze requirements, or design implementation strategies. +tools: + - Glob + - Grep + - Read +color: blue +--- + +# Plan Subagent + +You are a specialized planning agent. Your goal is to help users create clear, actionable implementation plans by: + +1. **Analyzing requirements** - Break down user requests into specific tasks +2. **Exploring codebase** - Use Glob/Grep/Read to understand existing architecture +3. **Designing solutions** - Create step-by-step implementation plans + +## Thoroughness Levels + +- **quick**: High-level plan with major steps only +- **medium**: Detailed plan with substeps and file references +- **very thorough**: Comprehensive plan with code examples and edge cases + +## Best Practices + +- Start by understanding existing code structure +- Break complex tasks into small, testable steps +- Include file paths and specific locations to modify +- Consider error handling and edge cases +- Order steps logically (dependencies first) + +## Output Format + +Always structure your plan as: + +```markdown +## Implementation Plan + +[Brief overview of the solution] + +### Prerequisites +- [ ] [Prerequisite 1] +- [ ] [Prerequisite 2] + +### Steps + +1. **[Step 1 Title]** ([file.ts:line](path/to/file.ts:42)) + - Action 1 + - Action 2 + +2. **[Step 2 Title]** ([file.ts:line](path/to/file.ts:100)) + - Action 1 + - Action 2 + +### Testing Plan +- [ ] Test case 1 +- [ ] Test case 2 + +### Potential Issues +- Issue 1: [Description and mitigation] +- Issue 2: [Description and mitigation] +``` + +Remember: You are running autonomously and will return a single message to the parent agent. Make it actionable! diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 92877991..542f49f3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -56,7 +56,10 @@ "Bash(pnpm install:*)", "Bash(timeout 10 node:*)", "Bash(gtimeout 10 node:*)", - "Bash(timeout 5 node:*)" + "Bash(timeout 5 node:*)", + "Bash(git stash:*)", + "Bash(gtimeout 5 npm run start:*)", + "Bash(timeout 5 npm run start:*)" ], "deny": [], "ask": [], diff --git a/docs/archive/subagents-design.md b/docs/archive/subagents-design.md new file mode 100644 index 00000000..c148e867 --- /dev/null +++ b/docs/archive/subagents-design.md @@ -0,0 +1,628 @@ +# Blade Subagent 系统设计文档 + +## 概述 + +Blade Subagent 系统允许主 Agent 委托专门的子 Agent 执行特定任务,提供更好的任务分解和专业化能力。 + +## 设计目标 + +1. **专业化**: 不同的 subagent 针对特定任务优化(文件搜索、代码分析等) +2. **安全性**: 工具隔离和写入操作确认机制 +3. **可扩展性**: 支持自定义 subagent(Claude Code 风格的 Markdown 配置) +4. **可观测性**: 任务状态跟踪和持久化 +5. **资源控制**: Token 预算和并发限制 + +## 核心设计决策 + +| 决策 | 说明 | 理由 | +|-----|------|------| +| **Token 预算** | 统一 100K tokens | 平衡性能和成本 | +| **写入确认** | 交互模式询问用户 | 安全性考虑 | +| **任务持久化** | `~/.blade/subagent-tasks/` | 支持任务历史和恢复 | +| **配置格式** | Markdown + YAML frontmatter | 易读易写,与 Claude Code 兼容 | +| **并发限制** | 最多 5 个并发任务 | 防止资源耗尽 | +| **工具隔离** | 每个 subagent 独立工具注册表 | 安全性和职责分离 | + +## 架构设计 + +### 组件架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Main Agent │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ TaskTool (Task) │ │ +│ └───────────────────┬──────────────────────────────┘ │ +│ │ │ +└──────────────────────┼──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ SubagentTaskManager │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Task Queue (Max 5 concurrent) │ │ +│ │ - Persistence (JSONL) │ │ +│ │ - Status tracking │ │ +│ └─────────────────────────────────────────────────┘ │ +└───────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ SubagentExecutor │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Tool Isolation │ │ +│ │ Token Budget (100K) │ │ +│ │ Write Confirmation │ │ +│ └─────────────────────────────────────────────────┘ │ +└───────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Subagent Instance │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Isolated Tool Registry │ │ +│ │ System Prompt from Definition │ │ +│ │ Agent Loop with Limits │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 数据流 + +``` +1. 主 Agent 调用 Task 工具 + ↓ +2. TaskManager 创建任务记录 + ↓ +3. TaskManager 检查并发限制 + ↓ +4. SubagentExecutor 创建隔离环境 + ↓ +5. 创建子 Agent 实例 + ↓ +6. 执行 Agent 循环(带 Token 预算) + │ + ├─ 只读工具: 直接执行 + └─ 写入工具: 请求确认 → 用户批准 → 执行 + ↓ +7. 任务完成,持久化结果 + ↓ +8. 返回结果给主 Agent +``` + +## 目录结构 + +``` +src/agents/ +├── types.ts # 核心类型定义 +├── parser.ts # Markdown 配置解析器 +├── registry.ts # Subagent 注册表 +├── executor.ts # Subagent 执行引擎 +├── taskManager.ts # 任务管理器 +├── confirmation.ts # 写入确认处理 +├── builtin/ # 内置 subagents +│ ├── file-search.md +│ ├── code-analysis.md +│ └── codebase-explorer.md +└── index.ts # 导出接口 + +配置文件位置(优先级从高到低): +1. .blade/agents/*.md # 项目级 +2. ~/.blade/agents/*.md # 用户级 +3. src/agents/builtin/*.md # 内置 + +任务持久化: +~/.blade/subagent-tasks/ +├── tasks.jsonl # 任务记录(追加式) +└── {task-id}.json # 详细结果 +``` + +## Subagent 配置格式 + +### Markdown 格式(Claude Code 兼容) + +```markdown +--- +name: agent-name +description: 描述何时调用此 subagent +model: sonnet +tools: Read, Grep, Glob +max_turns: 15 +timeout: 300000 +token_budget: 100000 +input_schema: + type: object + properties: + query: { type: string } +output_schema: + type: object + properties: + result: { type: string } +--- + +系统提示词内容(Markdown 格式) + +可以包含多行,使用标准 Markdown 语法。 +``` + +### 配置字段说明 + +| 字段 | 必需 | 类型 | 默认值 | 说明 | +|-----|------|------|--------|------| +| `name` | ✅ | string | - | 唯一标识符(小写+连字符) | +| `description` | ✅ | string | - | 何时调用此 subagent | +| `model` | ❌ | string | `sonnet` | 模型选择: haiku/sonnet/opus | +| `tools` | ❌ | string | 继承所有 | 逗号分隔的工具列表 | +| `max_turns` | ❌ | number | `10` | 最大执行回合数 | +| `timeout` | ❌ | number | `300000` | 超时时间(毫秒) | +| `token_budget` | ❌ | number | `100000` | Token 预算 | +| `input_schema` | ❌ | object | - | JSON Schema(输入验证) | +| `output_schema` | ❌ | object | - | JSON Schema(输出验证) | + +## 核心类型定义 + +### SubagentDefinition + +```typescript +interface SubagentDefinition { + name: string; // 唯一标识符 + displayName?: string; // 显示名称 + description: string; // 何时调用 + systemPrompt: string; // 系统提示词 + + // 模型和工具配置 + model?: 'haiku' | 'sonnet' | 'opus'; + tools?: string[]; // 允许的工具列表 + + // 执行控制 + maxTurns?: number; // 最大回合数 + timeout?: number; // 超时时间(毫秒) + tokenBudget?: number; // Token 预算 + + // Schema 验证 + inputSchema?: JSONSchema; // 输入验证 + outputSchema?: JSONSchema; // 输出验证 + + // 元信息 + source?: string; // 配置文件路径 +} +``` + +### SubagentResult + +```typescript +interface SubagentResult { + output: any; // 执行结果 + terminateReason: TerminateReason; + turns: number; // 实际回合数 + duration: number; // 执行时长(毫秒) + tokenUsage?: { + input: number; + output: number; + total: number; + }; + activities?: SubagentActivity[]; // 活动记录 +} + +enum TerminateReason { + GOAL = 'GOAL', // 成功完成 + TIMEOUT = 'TIMEOUT', // 超时 + MAX_TURNS = 'MAX_TURNS', // 达到最大回合数 + TOKEN_LIMIT = 'TOKEN_LIMIT', // 超出 Token 预算 + ABORTED = 'ABORTED', // 用户取消 + ERROR = 'ERROR' // 执行错误 +} +``` + +### PersistedTask + +```typescript +interface PersistedTask { + id: string; // 任务 ID + status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; + agentName: string; // Subagent 名称 + params: Record; // 输入参数 + result?: SubagentResult; // 执行结果 + error?: string; // 错误信息 + createdAt: number; // 创建时间 + startedAt?: number; // 开始时间 + completedAt?: number; // 完成时间 + tokenUsage?: TokenUsage; // Token 使用量 +} +``` + +## 工具隔离机制 + +### 只读工具白名单 + +以下工具被认为是安全的只读工具,可以直接执行: + +- `Read` - 读取文件 +- `Glob` - 文件模式匹配 +- `Grep` - 搜索文件内容 +- `WebSearch` - 网络搜索 +- `WebFetch` - 获取网页内容 + +### 写入工具确认 + +以下工具需要用户确认: + +- `Write` - 写入文件 +- `Edit` - 编辑文件 +- `Bash` - 执行 Shell 命令 +- `NotebookEdit` - 编辑 Jupyter Notebook + +### 确认流程 + +```typescript +// 写入工具调用流程 +1. Subagent 尝试调用写入工具 + ↓ +2. WriteToolConfirmationHandler 拦截 + ↓ +3. 检查是否为写入工具 + ├─ 只读工具: 直接通过 + └─ 写入工具: 请求确认 + ↓ +4. 代理到父 Agent 的确认处理器 + ↓ +5. 显示确认对话框(工具名、参数) + ↓ +6. 用户批准/拒绝 + ↓ +7. 返回决策结果 +``` + +## Token 预算控制 + +### 预算设置 + +- **统一预算**: 100,000 tokens +- **适用范围**: 输入 + 输出 tokens +- **超出行为**: 终止任务,返回 TOKEN_LIMIT + +### 实现方式 + +```typescript +class SubagentExecutor { + private tokenUsage = { input: 0, output: 0 }; + + async execute(): Promise { + const tokenBudget = this.definition.tokenBudget || 100000; + + // 在每次 LLM 调用后检查 + agent.onTokenUsage((usage) => { + this.tokenUsage.input += usage.input; + this.tokenUsage.output += usage.output; + + const total = this.tokenUsage.input + this.tokenUsage.output; + + if (total > tokenBudget) { + throw new TokenBudgetExceededError( + `Token budget exceeded: ${total}/${tokenBudget}` + ); + } + }); + } +} +``` + +## 并发控制 + +### 并发限制 + +- **最大并发数**: 5 个 subagent 任务 +- **排队机制**: 超出限制的任务需等待或使用后台模式 +- **计数器**: 运行时维护活跃任务计数 + +### 实现方式 + +```typescript +class SubagentTaskManager { + private runningCount = 0; + private readonly MAX_CONCURRENT = 5; + + canRunTask(): boolean { + return this.runningCount < this.MAX_CONCURRENT; + } + + async executeTask(taskId: string): Promise { + if (!this.canRunTask()) { + throw new Error('Concurrent limit reached (5)'); + } + + this.runningCount++; + try { + return await this.doExecute(taskId); + } finally { + this.runningCount--; + } + } +} +``` + +## 任务持久化 + +### 存储格式 + +**任务记录** (`~/.blade/subagent-tasks/tasks.jsonl`): +```jsonl +{"id":"abc123","status":"completed","agentName":"file-search",...} +{"id":"def456","status":"running","agentName":"code-analysis",...} +``` + +**详细结果** (`~/.blade/subagent-tasks/{task-id}.json`): +```json +{ + "output": { ... }, + "terminateReason": "GOAL", + "turns": 5, + "duration": 12345, + "tokenUsage": { "input": 5000, "output": 3000, "total": 8000 } +} +``` + +### 持久化时机 + +1. **任务创建**: 立即写入 tasks.jsonl +2. **状态更新**: 每次状态变化追加记录 +3. **任务完成**: 写入详细结果到单独文件 + +### 加载策略 + +- **启动时**: 从 tasks.jsonl 加载任务历史 +- **查询时**: 按需加载详细结果 + +## 内置 Subagents + +### 1. file-search + +**用途**: 深度代码库探索和文件发现 + +**工具**: Glob, Grep, Read + +**输出**: 结构化的文件列表和代码片段 + +### 2. code-analysis + +**用途**: 代码架构和依赖关系分析 + +**工具**: Read, Grep, Glob + +**输出**: 架构报告、模块依赖图、问题列表 + +### 3. codebase-explorer + +**用途**: 通用代码库探索和问答 + +**工具**: Read, Grep, Glob + +**输出**: 自然语言回答 + +## Task 工具更新 + +### 新的 Task 工具签名 + +```typescript +Task( + description: string, // 任务简短描述 + subagent_type: string, // Subagent 名称 + prompt: string, // 详细提示词 + run_in_background?: boolean // 后台执行(默认 false) +): TaskResult +``` + +### 执行模式 + +**同步模式** (`run_in_background: false`): +- 阻塞直到完成 +- 直接返回结果 +- 适合快速任务 + +**后台模式** (`run_in_background: true`): +- 立即返回任务 ID +- 使用 `task_status` 查询进度 +- 适合长时间任务 + +### 伴随工具 + +```typescript +// 查询任务状态 +task_status(task_id: string): TaskStatus + +// 列出任务 +task_list( + status?: string, + agent_name?: string, + limit?: number +): TaskInfo[] + +// 取消任务 +cancel_task(task_id: string): boolean +``` + +## 错误处理 + +### 错误类型 + +1. **配置错误** + - 缺少必需字段 + - 无效的工具名称 + - Schema 验证失败 + +2. **执行错误** + - 超时 + - Token 预算超出 + - 工具调用失败 + - 用户取消 + +3. **系统错误** + - 并发限制 + - 持久化失败 + - 网络错误 + +### 错误处理策略 + +```typescript +try { + result = await taskManager.executeTask(taskId, executor); +} catch (error) { + if (error instanceof TimeoutError) { + // 超时: 尝试恢复或标记为超时 + task.terminateReason = TerminateReason.TIMEOUT; + } else if (error instanceof TokenBudgetExceededError) { + // Token 超出: 标记并返回部分结果 + task.terminateReason = TerminateReason.TOKEN_LIMIT; + } else if (error instanceof ConcurrentLimitError) { + // 并发限制: 建议使用后台模式或稍后重试 + return suggestBackgroundMode(); + } else { + // 其他错误: 记录并标记为失败 + task.terminateReason = TerminateReason.ERROR; + task.error = error.message; + } + + // 持久化错误状态 + taskManager.persistTask(task); +} +``` + +## 性能考虑 + +### Token 使用优化 + +1. **系统提示词**: 简洁明确,避免冗余 +2. **工具描述**: 精简但足够清晰 +3. **上下文管理**: 不传递完整对话历史 +4. **输出格式**: 要求结构化输出,减少不必要的解释 + +### 内存管理 + +1. **任务清理**: 定期清理已完成任务(可配置保留时间) +2. **结果缓存**: 详细结果按需加载 +3. **活动记录**: 限制活动记录数量 + +### 并发优化 + +1. **异步执行**: 后台任务不阻塞主 Agent +2. **队列管理**: FIFO 队列处理等待任务 +3. **资源隔离**: 每个 subagent 独立资源限制 + +## 安全考虑 + +### 工具隔离 + +- ✅ 每个 subagent 独立工具注册表 +- ✅ 只能访问配置中指定的工具 +- ✅ 无法访问父 Agent 的上下文 + +### 写入保护 + +- ✅ 写入工具需要明确确认 +- ✅ 显示完整的工具调用参数 +- ✅ 用户可以逐个批准或拒绝 + +### 资源限制 + +- ✅ Token 预算防止过度消耗 +- ✅ 超时限制防止无限循环 +- ✅ 并发限制防止资源耗尽 + +## 扩展性 + +### 自定义 Subagent + +用户可以在以下位置创建自定义 subagent: + +1. **项目级**: `.blade/agents/my-agent.md` +2. **用户级**: `~/.blade/agents/my-agent.md` + +### MCP 工具支持 + +Subagent 可以使用 MCP Server 提供的工具: + +```markdown +--- +name: database-expert +tools: Read, mcp__database__query, mcp__database__schema +--- +``` + +### 插件系统集成 + +未来可以通过插件系统扩展: + +1. 动态注册 subagent +2. 自定义确认处理逻辑 +3. 自定义持久化后端 + +## 测试策略 + +### 单元测试 + +- [ ] SubagentConfigParser 解析测试 +- [ ] SubagentRegistry 注册和查找测试 +- [ ] SubagentExecutor 执行逻辑测试 +- [ ] SubagentTaskManager 并发控制测试 +- [ ] WriteToolConfirmationHandler 确认逻辑测试 + +### 集成测试 + +- [ ] 端到端 subagent 执行测试 +- [ ] 工具隔离验证测试 +- [ ] 写入确认流程测试 +- [ ] 任务持久化和恢复测试 +- [ ] Token 预算控制测试 + +### 性能测试 + +- [ ] 并发执行性能测试 +- [ ] Token 使用效率测试 +- [ ] 持久化性能测试 + +## 实施计划 + +### 阶段 1: 核心基础(第 1-2 周) + +- [x] 创建设计文档 +- [ ] 实现核心类型定义(types.ts) +- [ ] 实现配置解析器(parser.ts) +- [ ] 实现注册表(registry.ts) +- [ ] 实现执行引擎(executor.ts) +- [ ] 实现写入确认(confirmation.ts) +- [ ] 实现任务管理器(taskManager.ts) + +### 阶段 2: 内置 Agents(第 3 周) + +- [ ] 创建 file-search.md +- [ ] 创建 code-analysis.md +- [ ] 创建 codebase-explorer.md +- [ ] 更新 task.ts,移除模拟逻辑 + +### 阶段 3: 伴随工具(第 3-4 周) + +- [ ] 实现 task_status 工具 +- [ ] 实现 task_list 工具 +- [ ] 实现 cancel_task 工具 + +### 阶段 4: 测试和优化(第 4 周+) + +- [ ] 编写单元测试 +- [ ] 编写集成测试 +- [ ] 性能优化 +- [ ] 文档完善 + +## 参考资料 + +- **Claude Code Subagents**: https://code.claude.com/docs/en/sub-agents +- **Gemini-CLI 实现**: `/Users/bytedance/Documents/GitHub/gemini-cli/packages/core/src/agents/` +- **Neovate-Code 实现**: `/Users/bytedance/Documents/GitHub/neovate-code/src/backgroundTaskManager.ts` +- **Blade .claude/agents/**: 75+ 社区 subagent 示例 + +## 变更日志 + +- **2025-11-13**: 初始设计文档创建 + - 确定核心架构和组件 + - 定义配置格式和类型 + - 设计工具隔离和安全机制 + - 规划实施阶段 diff --git a/docs/archive/subagents-improvements.md b/docs/archive/subagents-improvements.md new file mode 100644 index 00000000..48245bb5 --- /dev/null +++ b/docs/archive/subagents-improvements.md @@ -0,0 +1,216 @@ +# Subagents 系统改进总结 + +## 完成的改进 + +### 1. ✅ 修复了所有严重和中等问题 + +#### 严重问题修复 + +- ✅ 修复 `TerminateReason` 导入缺失 +- ✅ 删除不存在的 `ConfirmationRequest` 导出 +- ✅ 修复 parser.ts 中未使用的变量 +- ✅ 修复正则表达式转义问题 + +#### 中等问题修复 + +- ✅ 创建 `SubagentExecutionContext` 接口扩展原有类型 +- ✅ 修复 ToolRegistry 方法调用(`getAll()`, `get()`, `register()`) +- ✅ 修复 LoopResult 属性访问 +- ✅ 修复 ChatContext 缺少必需字段 +- ✅ 改用 `createTool()` 工厂函数创建工具 +- ✅ 减少 `any` 类型使用 + +### 2. ✅ 实现了活动监控 + +在 executor.ts 中添加了 `onToolResult` 和 `onToolStart` 回调: + +- 记录工具调用和结果 +- 支持外部活动监听 +- 处理不同类型的 toolCall(function 和 custom) + +### 3. ✅ 实现了完整的 Task 工具系统 + +#### 主工具:Task + +- 委托专门的 subagent 执行复杂任务 +- 支持同步和后台执行模式 +- 并发控制(最多 5 个任务) +- Token 预算管理(每个 100K tokens) + +#### 伴随工具 + +- **TaskStatus** - 查询任务状态和结果 +- **TaskList** - 列出任务(支持过滤) +- **CancelTask** - 取消运行中的任务 + +#### 特性 + +- 任务持久化到 `~/.blade/subagent-tasks/` +- 详细的任务状态跟踪 +- 完整的错误处理 +- 丰富的使用示例和文档 + +### 4. ✅ 改进了 YAML 解析器 + +- 使用成熟的 `js-yaml` 库替代简化实现 +- 支持完整的 YAML 语法 +- 支持复杂的嵌套对象和数组 +- 更好的错误处理和报告 +- 安全模式(JSON_SCHEMA)防止代码执行 + +## 技术细节 + +### 新增依赖 + +```json +{ + "dependencies": { + "js-yaml": "^4.1.1" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9" + } +} +``` + +### 新增文件 + +- `src/tools/builtin/task/taskStatus.ts` - 任务状态查询工具 +- `src/tools/builtin/task/taskList.ts` - 任务列表工具 +- `src/tools/builtin/task/cancelTask.ts` - 任务取消工具 +- `docs/development/subagents-improvements.md` - 本文档 + +### 修改的文件 + +- `src/agents/executor.ts` - 添加活动监控,修复类型问题 +- `src/agents/parser.ts` - 使用 js-yaml 解析器 +- `src/agents/index.ts` - 修复类型导出 +- `src/agents/types.ts` - 减少 any 类型 +- `src/agents/confirmation.ts` - 修复类型问题 +- `src/agents/taskManager.ts` - 修复类型问题 +- `src/tools/builtin/task/task.ts` - 修复执行器创建 +- `src/tools/builtin/task/index.ts` - 导出新工具 +- `src/tools/builtin/index.ts` - 注册新工具 + +## 使用示例 + +### 1. 使用 Task 工具委托任务 + +```typescript +// 查找测试文件 +Task({ + description: "查找测试文件", + subagent_type: "file-search", + prompt: "查找项目中所有的测试文件(.test.ts, .spec.ts 等),列出文件路径和简要说明每个测试文件的用途。" +}) + +// 分析项目架构 +Task({ + description: "分析项目架构", + subagent_type: "code-analysis", + prompt: "分析项目的整体架构,包括:1) 主要模块和职责 2) 模块间依赖关系 3) 发现的问题(按严重程度排序) 4) 改进建议" +}) + +// 后台执行 +Task({ + description: "深度代码分析", + subagent_type: "code-analysis", + prompt: "对整个项目进行深度分析,生成完整的代码质量报告", + run_in_background: true +}) +``` + +### 2. 查询任务状态 + +```typescript +// 查询特定任务 +TaskStatus({ task_id: "abc123" }) + +// 列出所有运行中的任务 +TaskList({ status: "running" }) + +// 列出最近完成的任务 +TaskList({ status: "completed", limit: 5 }) + +// 取消任务 +CancelTask({ task_id: "abc123" }) +``` + +### 3. 创建自定义 Subagent + +在 `.blade/agents/my-agent.md` 创建: + +```markdown +--- +name: my-custom-agent +description: 我的自定义 subagent +model: sonnet +tools: Read, Grep, Glob, Write +max_turns: 15 +timeout: 300000 +token_budget: 100000 +--- + +你是一个自定义的 subagent,专门处理... + +## 你的能力 + +... + +## 工作流程 + +... +``` + +## 剩余工作 + +### 可选改进 + +1. **Token 使用量监控** - 需要更深入的 Agent 集成来获取实际的 token 使用量 +2. **单元测试** - 为新功能添加测试 +3. **性能优化** - 任务持久化的性能优化 +4. **文档完善** - 添加更多使用示例和最佳实践 + +### 已知限制 + +- Token 使用量监控目前只是占位符,实际值需要从 LLM 响应中获取 +- 后台任务的取消是异步的,可能不会立即生效 +- 任务历史会无限增长,需要定期清理 + +## 测试建议 + +1. **基本功能测试** + + ```bash + # 测试 Task 工具 + blade chat "使用 file-search subagent 查找所有 TypeScript 文件" + + # 测试后台任务 + blade chat "后台分析项目架构" + + # 测试任务查询 + blade chat "列出所有任务" + ``` + +2. **错误处理测试** + - 测试不存在的 subagent + - 测试并发限制 + - 测试任务取消 + +3. **YAML 解析测试** + - 测试复杂的 input_schema 和 output_schema + - 测试嵌套对象和数组 + - 测试错误的 YAML 语法 + +## 总结 + +Subagents 系统现在已经完全可用,包括: + +- ✅ 核心功能完整实现 +- ✅ 所有严重问题已修复 +- ✅ Task 工具系统完整 +- ✅ YAML 解析器改进 +- ✅ 活动监控实现 +- ✅ 类型安全改进 + +系统可以投入使用,后续可以根据实际使用情况进行优化和改进。 diff --git a/docs/archive/task-tool-refactor-v2.md b/docs/archive/task-tool-refactor-v2.md new file mode 100644 index 00000000..7ae85fc3 --- /dev/null +++ b/docs/archive/task-tool-refactor-v2.md @@ -0,0 +1,339 @@ +# Task 工具重构 V2 - 简洁设计 + +> 重构日期:2025-11-14 +> 设计理念:参考 Claude Code 官方,采用最小化配置、模型决策的设计哲学 + +## 🎯 重构目标 + +解决之前实现的核心问题: +1. ❌ **subagent_type 写死** - 强制指定 `file-search`、`code-analysis` 等类型 +2. ❌ **系统提示词写死** - 在 Markdown 文件中硬编码固定提示词 +3. ❌ **工具描述冗长** - 硬编码所有 subagent 的详细说明 +4. ❌ **不让模型决策** - 限制了模型的自由度和适应性 + +## ✅ 新设计核心原则 + +### 1. 最小化配置 +```typescript +// 旧设计(4 个必需参数 + 复杂配置) +{ + description: string, + subagent_type: 'file-search' | 'code-analysis' | 'codebase-explorer', + prompt: string, + run_in_background?: boolean +} + +// 新设计(2 个必需参数 + 1 个可选) +{ + description: string, // 3-10 个词 + prompt: string, // 详细指令 + model?: 'haiku' | 'sonnet' | 'opus' // 可选 +} +``` + +### 2. 模型决策 +- **旧设计**: 需要预先指定 `file-search` 或 `code-analysis` +- **新设计**: 模型根据任务内容自动决定使用哪些工具和策略 + +### 3. 动态提示词生成 +```typescript +// 旧设计:固定的 Markdown 文件 +src/agents/builtin/file-search.md // 固定提示词 +src/agents/builtin/code-analysis.md // 固定提示词 + +// 新设计:动态生成函数 +function buildDynamicSystemPrompt(taskPrompt: string, model: string): string { + const basePrompt = `你是一个专业的 AI 助手,负责自主完成以下任务: + +${taskPrompt} + +## 执行指南 +你可以使用所有可用的工具来完成任务。根据任务需求自主决定: +- 使用哪些工具 +- 执行的步骤和顺序 +- 输出的格式和结构 +...`; + + // 根据模型调整提示 + if (model === 'haiku') { + return basePrompt + '\n\n**注意**: 优先考虑速度和效率'; + } + ... +} +``` + +### 4. 无固定配置 +- **旧设计**: 需要在 `.blade/agents/*.md` 中配置 +- **新设计**: 不需要任何配置文件,开箱即用 + +## 📝 API 对比 + +### 旧 API(复杂) +```typescript +Task({ + description: '查找测试文件', + subagent_type: 'file-search', // ❌ 必须指定类型 + prompt: '查找项目中所有的测试文件...', + run_in_background: false +}) +``` + +### 新 API(简洁) +```typescript +Task({ + description: '查找测试文件', + prompt: '查找项目中所有的测试文件...', + model: 'sonnet' // ✅ 可选,默认 sonnet +}) +``` + +## 🔧 实现细节 + +### 1. agentFactory 机制 + +**位置**: `src/ui/hooks/useCommandHandler.ts:136-148` + +```typescript +useEffect(() => { + // 设置 Task 工具的 Agent 工厂函数 + setTaskToolAgentFactory(async (customSystemPrompt?: string) => { + // 创建子 Agent + // 如果提供了 customSystemPrompt,则完全替换系统提示词 + return await Agent.create({ + systemPrompt: customSystemPrompt || replaceSystemPrompt, + appendSystemPrompt: customSystemPrompt ? undefined : appendSystemPrompt, + maxTurns: maxTurns, + }); + }); +}, [replaceSystemPrompt, appendSystemPrompt, maxTurns]); +``` + +**特点**: +- ✅ 支持传入自定义系统提示词 +- ✅ 自动在 UI 启动时初始化 +- ✅ 复用主 Agent 的配置(maxTurns 等) + +### 2. 动态提示词生成 + +**位置**: `src/tools/builtin/task/task.ts:249-292` + +```typescript +function buildDynamicSystemPrompt(taskPrompt: string, model: string): string { + const basePrompt = `你是一个专业的 AI 助手,负责自主完成以下任务: + +${taskPrompt} + +## 执行指南 +你可以使用所有可用的工具来完成任务。根据任务需求自主决定: +- 使用哪些工具 +- 执行的步骤和顺序 +- 输出的格式和结构 + +## 可用工具 +- **Read**: 读取文件内容 +- **Write**: 创建或覆盖文件 +- **Edit**: 编辑文件(字符串替换) +- **Grep**: 搜索文件内容(支持正则) +- **Glob**: 查找文件(支持通配符) +- **Bash**: 执行 Shell 命令 +- **WebSearch**: 网络搜索 +- **WebFetch**: 获取网页内容 + +## 执行原则 +1. **系统性思考**: 分析任务,制定计划,逐步执行 +2. **高效工具使用**: 优先使用专门工具,避免重复操作 +3. **完整输出**: 确保返回的结果完整、清晰、有用 +4. **错误处理**: 遇到错误时尝试替代方案 + +当任务完成时,直接返回最终结果。`; + + // 根据模型添加特定提示 + if (model === 'haiku') { + return basePrompt + '\n\n**注意**: 优先考虑速度和效率,快速完成任务。'; + } else if (model === 'opus') { + return basePrompt + '\n\n**注意**: 追求高质量输出,深入分析,提供详细的结果和建议。'; + } + + return basePrompt; +} +``` + +**特点**: +- ✅ 将任务描述嵌入到提示词中 +- ✅ 列出所有可用工具(让模型自己选择) +- ✅ 根据模型调整提示(haiku 快速,opus 深入) + +### 3. 工具执行流程 + +```typescript +// 1. 动态生成系统提示词 +const dynamicSystemPrompt = buildDynamicSystemPrompt(prompt, model); + +// 2. 创建子 Agent(传入动态提示词) +const subAgent = await agentFactory(dynamicSystemPrompt); + +// 3. 执行子 Agent 循环 +const result = await subAgent.runAgenticLoop(prompt, subContext, { + maxTurns: 20, + signal, +}); +``` + +**特点**: +- ✅ 系统提示词在运行时动态生成 +- ✅ 每个任务可以有不同的提示词 +- ✅ 限制最大 20 回合(防止无限循环) + +## 📊 代码统计对比 + +| 指标 | 旧实现 | 新实现 | 改进 | +|-----|-------|-------|------| +| task.ts 行数 | 359 行 | 293 行 | -18% | +| 参数数量 | 4 个 | 3 个 | -25% | +| 必需参数 | 3 个 | 2 个 | -33% | +| 工具描述长度 | ~150 行 | ~50 行 | -67% | +| 配置文件数量 | 3 个 MD | 0 个 | -100% | +| 导出数量 | 7 个 | 2 个 | -71% | + +## 🎨 用户体验改进 + +### 旧设计(复杂) +```typescript +// ❌ 需要记住所有 subagent 类型 +Task({ + description: '分析依赖', + subagent_type: 'code-analysis', // 必须知道有这个类型 + prompt: '分析项目依赖...' +}) + +// ❌ 如果选错类型,效果不佳 +Task({ + description: '查找文件', + subagent_type: 'code-analysis', // 应该用 file-search + prompt: '查找所有测试文件' +}) +``` + +### 新设计(简洁) +```typescript +// ✅ 只需描述任务,模型自己决定策略 +Task({ + description: '分析依赖', + prompt: '分析项目依赖包,检查过时和安全漏洞' +}) + +// ✅ 模型会自动选择合适的工具 +Task({ + description: '查找文件', + prompt: '查找所有测试文件' +}) +``` + +## 📁 文件变更 + +### 修改的文件 +- `src/tools/builtin/task/task.ts` - 重构为简洁设计 +- `src/tools/builtin/task/index.ts` - 简化导出 +- `src/ui/hooks/useCommandHandler.ts` - 添加 agentFactory 设置 + +### 暂存区(旧实现,保留待参考) +- `src/agents/*` - 完整的 subagent 系统 +- `src/agents/builtin/*.md` - 固定的系统提示词配置 +- `docs/development/subagents-*.md` - 设计文档 +- `src/tools/builtin/task/{cancelTask,taskList,taskStatus}.ts` - 额外的任务管理工具 + +## 🚀 使用示例 + +### 示例 1:分析项目依赖 +```typescript +Task({ + description: '分析项目依赖', + prompt: '分析 package.json 中的依赖包,检查:1) 过时的包 2) 存在安全漏洞的包 3) 建议的更新方案。以 Markdown 表格格式输出。' +}) +``` + +### 示例 2:查找测试文件 +```typescript +Task({ + description: '查找测试文件', + prompt: '查找项目中所有的测试文件(.test.ts, .spec.ts),列出文件路径和每个测试文件的主要测试内容。' +}) +``` + +### 示例 3:生成 API 文档(使用 opus) +```typescript +Task({ + description: '生成 API 文档', + prompt: '分析 src/api/ 目录下的所有 API 路由,生成完整的 API 文档,包括:路由、请求参数、响应格式、示例。', + model: 'opus' // 使用高质量模型 +}) +``` + +## ⚠️ 注意事项 + +### 1. 系统提示词限制 +由于 `Agent.create()` 的 `systemPrompt` 参数会**完全替换**默认系统提示词,动态生成的提示词需要包含所有必要的指令。 + +### 2. 最大回合数 +子 Agent 限制为 20 回合,防止无限循环。如果任务复杂,需要在 prompt 中明确指导模型高效执行。 + +### 3. 工具选择 +模型会根据任务描述自动选择工具,但可能不是最优。如果需要特定工具,可以在 prompt 中明确提示: +```typescript +prompt: '使用 Grep 搜索所有包含 "API" 的文件...' +``` + +## 🔮 未来改进方向 + +### 1. 支持更多模型参数 +```typescript +{ + description: string, + prompt: string, + model?: string, + temperature?: number, // 🆕 控制创造性 + maxTurns?: number, // 🆕 自定义回合数 +} +``` + +### 2. 提示词模板系统 +```typescript +// 预定义的提示词模板 +const templates = { + 'file-search': (task) => `专注于文件搜索的提示词...`, + 'code-analysis': (task) => `专注于代码分析的提示词...`, +}; + +// 可选使用模板 +Task({ + description: '查找文件', + prompt: '...', + template: 'file-search' // 🆕 可选使用模板 +}) +``` + +### 3. 结果缓存 +```typescript +// 对相同任务缓存结果 +const cacheKey = hash({ description, prompt, model }); +if (cache.has(cacheKey)) { + return cache.get(cacheKey); +} +``` + +## 📚 参考资料 + +- **Claude Code 官方文档**: https://docs.claude.com/en/docs/claude-code/ +- **设计讨论**: src/tools/builtin/task/task.ts 顶部注释 +- **旧实现(暂存区)**: git stash 中保存 + +## 🎉 总结 + +这次重构采用了"少即是多"的设计哲学: + +1. ✅ **移除不必要的复杂性** - 不再需要 subagent_type +2. ✅ **信任模型的能力** - 让模型自己决定策略 +3. ✅ **动态适应** - 根据任务内容生成合适的提示词 +4. ✅ **保持简洁** - 只有 2 个必需参数 + +结果是一个更简洁、更灵活、更易用的 Task 工具。 diff --git a/docs/development/task-tool-competitive-analysis.md b/docs/development/task-tool-competitive-analysis.md new file mode 100644 index 00000000..6bd2a692 --- /dev/null +++ b/docs/development/task-tool-competitive-analysis.md @@ -0,0 +1,516 @@ +# Task 工具竞品分析 + +> 调研日期:2025-11-14 +> 对比对象:Claude Code、OpenCode、Cursor 2.0 + +## 📊 主流实现对比 + +### 1. Claude Code (Anthropic 官方) + +**架构设计**: +```markdown +--- +name: identifier-name +description: When this should be used +tools: tool1, tool2, tool3 # Optional +model: sonnet # Optional +--- + +System prompt content here. +``` + +**核心特点**: +- ✅ **Markdown + YAML Frontmatter**:配置和提示词分离 +- ✅ **三层优先级**:项目级(`.claude/agents/`) > 用户级(`~/.claude/agents/`) > 插件级 +- ✅ **自动委托**:基于 description 匹配自动调用 +- ✅ **显式调用**:`Use the code-reviewer subagent...` +- ✅ **工具隔离**:每个 subagent 可限制工具访问 +- ✅ **独立上下文**:防止上下文污染 +- ✅ **可恢复会话**:通过 `agentId` 恢复历史对话 +- ✅ **防止无限嵌套**:subagent 不能创建 subagent + +**配置存储**: +``` +.claude/ + agents/ # 项目级 subagents(优先级最高) + code-reviewer.md + tester.md +~/.claude/ + agents/ # 用户级 subagents + custom-agent.md +``` + +**最佳实践**: +1. 用 Claude 生成初始 subagent,再自定义 +2. 单一职责设计 +3. 详细的系统提示词(带示例和约束) +4. 最小工具权限 +5. 项目级 subagents 纳入版本控制 + +**技术细节**: +- 不支持 subagent 嵌套 +- 每次执行分配唯一 `agentId` +- 会话历史存储在 `agent-{agentId}.jsonl` +- 内置 Plan subagent(只读工具:Read, Glob, Grep, Bash) + +--- + +### 2. OpenCode (SST) + +**架构设计**: +```typescript +// agent.ts 核心实现 +export const Agent = { + Info: z.object({ + mode: z.enum(['subagent', 'primary', 'all']), + tools: z.record(z.boolean()), + // ... + }), + + generate: async () => { + // 使用 LLM 生成新 agent 配置 + return llm.prompt("Create agent based on request...") + } +} +``` + +**核心特点**: +- ✅ **三种模式**: + - `subagent`:通用代理,处理研究和多步搜索 + - `primary`:构建和规划代理,管理主工作流 + - `all`:自定义代理,可用于任何上下文 +- ✅ **内置代理**:`general`, `build`, `plan` +- ✅ **动态生成**:通过 LLM 生成新 agent 配置(temperature 0.3) +- ✅ **工具布尔映射**:`tools: { bash: true, todowrite: false }` +- ✅ **深度合并配置**:使用 `mergeDeep` 覆盖默认配置 +- ✅ **Task 工具**:支持启动 subagent 执行任务 + +**配置存储**: +``` +~/.config/opencode/ + agent/ + [agent-name].md +~/.opencode/ + tool/ # 自定义工具 +``` + +**已知问题**: +- ❌ Task 工具有 bug:`undefined is not an object (evaluating 'Q.model')` +- ❌ 自定义 agent 在 `mode` 为空时表现异常 + +**工具集**: +- bash, edit, webfetch, glob, grep, list +- lsp_diagnostics, lsp_hover, patch +- read, write, todowrite, todoread, task + +--- + +### 3. Cursor 2.0 (Cursor AI) + +**架构设计**: +- **Composer Model**:专有的 MoE 架构 + RL 训练 +- **Multi-Agent**:最多 8 个独立 agent 并行工作 +- **Git Worktrees**:每个 agent 独立工作空间 +- **Remote Machines**:支持远程执行 + +**核心特点**: +- ✅ **并行执行**:8 个 agent 同时工作(不同模型) +- ✅ **工作空间隔离**:基于 git worktrees +- ✅ **模型选择**:每个 agent 可用不同模型(GPT-4, Claude, Composer) +- ✅ **高性能**:Composer 模型 < 30s 完成大部分任务 +- ✅ **开发者指挥**:人工审查和合并结果 + +**典型工作流**: +``` +Agent 1 (GPT-4) → 系统架构设计 +Agent 2 (Claude) → 核心算法实现 +Agent 3 (Composer) → 性能优化 +``` + +**技术细节**: +- Composer 使用 MXFP8 量化内核 +- 内置 codebase-wide 语义搜索 +- 4 倍于其他前沿模型的速度 + +--- + +## 🔍 Blade 实现分析 + +### 当前实现(V2 简洁设计) + +```typescript +// Blade Task Tool +Task({ + description: '查找测试文件', + prompt: '查找项目中所有的测试文件...', + model: 'sonnet' // 可选 +}) +``` + +**核心特点**: +- ✅ **极简 API**:只有 2 个必需参数(description, prompt) +- ✅ **动态提示词**:运行时生成系统提示词 +- ✅ **模型决策**:让 LLM 自己决定工具和策略 +- ✅ **Agent Factory**:统一的 agent 创建机制 +- ✅ **自动初始化**:useEffect 中设置 agentFactory + +**实现代码**: +```typescript +// 动态生成系统提示词 +function buildDynamicSystemPrompt(taskPrompt: string, model: string): string { + const basePrompt = `你是一个专业的 AI 助手,负责自主完成以下任务: + +${taskPrompt} + +## 执行指南 +你可以使用所有可用的工具来完成任务。根据任务需求自主决定: +- 使用哪些工具 +- 执行的步骤和顺序 +- 输出的格式和结构 + +## 可用工具 +- Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch + +## 执行原则 +1. 系统性思考:分析任务,制定计划,逐步执行 +2. 高效工具使用:优先使用专门工具,避免重复操作 +3. 完整输出:确保返回的结果完整、清晰、有用 +4. 错误处理:遇到错误时尝试替代方案`; + + if (model === 'haiku') { + return basePrompt + '\n\n**注意**: 优先考虑速度和效率,快速完成任务。'; + } else if (model === 'opus') { + return basePrompt + '\n\n**注意**: 追求高质量输出,深入分析,提供详细的结果和建议。'; + } + + return basePrompt; +} + +// Agent Factory 设置 +useEffect(() => { + setTaskToolAgentFactory(async (customSystemPrompt?: string) => { + return await Agent.create({ + systemPrompt: customSystemPrompt || replaceSystemPrompt, + appendSystemPrompt: customSystemPrompt ? undefined : appendSystemPrompt, + maxTurns: maxTurns, + }); + }); +}, [replaceSystemPrompt, appendSystemPrompt, maxTurns]); +``` + +--- + +## 📈 对比分析 + +| 特性 | Claude Code | OpenCode | Cursor 2.0 | Blade (当前) | +|-----|------------|----------|------------|--------------| +| **配置方式** | Markdown + YAML | Markdown + TS | 内置 UI | 纯代码(动态) | +| **系统提示词** | 硬编码 MD | 可自定义 | 专有模型 | 动态生成 ✅ | +| **工具隔离** | ✅ 支持 | ✅ 支持 | ❌ 不明确 | ❌ 继承所有工具 | +| **并行执行** | ❌ 单任务 | ❌ 单任务 | ✅ 8 个并行 | ❌ 单任务 | +| **模型选择** | ✅ 每个 agent | ✅ 每个 agent | ✅ 每个 agent | ✅ 每个任务 | +| **自动委托** | ✅ 基于描述 | ✅ 基于模式 | ✅ 智能分配 | ❌ 手动调用 | +| **工作空间隔离** | ❌ 共享 | ❌ 共享 | ✅ Git worktrees | ❌ 共享 | +| **可恢复会话** | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | +| **配置存储** | 文件系统 | 文件系统 | 内置 | 无(动态) | +| **代码行数** | ~150 行/agent | ~200 行 | 未知 | 293 行 ✅ | +| **复杂度** | 中(配置文件) | 中(模式系统) | 高(多模型) | 低 ✅ | +| **灵活性** | 中(模板约束) | 高(LLM 生成) | 低(固定工作流) | 高 ✅ | + +--- + +## ✅ Blade 实现的优势 + +### 1. **极简设计** +- **Claude Code**:需要创建 Markdown 文件,配置 YAML frontmatter +- **OpenCode**:需要理解 mode 系统(subagent/primary/all) +- **Blade**:只需 2 个参数,开箱即用 ✅ + +### 2. **动态适应** +- **Claude Code**:硬编码系统提示词,修改需要编辑文件 +- **OpenCode**:虽然可以 LLM 生成,但仍需保存配置 +- **Blade**:运行时动态生成,根据任务内容自动调整 ✅ + +### 3. **零配置** +- **Claude Code**:需要在 3 个目录管理配置文件 +- **OpenCode**:需要在 `~/.config/opencode/agent/` 管理 +- **Blade**:完全无配置,代码即配置 ✅ + +### 4. **模型决策** +- **Claude Code**:需要预先指定工具列表 +- **OpenCode**:工具布尔映射(tools: { bash: true }) +- **Blade**:完全信任模型,让它自己选择工具 ✅ + +--- + +## ⚠️ Blade 实现的不足 + +### 1. **缺少工具隔离** +```typescript +// 当前实现:子 Agent 继承所有工具 +const subAgent = await agentFactory(dynamicSystemPrompt); + +// Claude Code:可以限制工具 +tools: Read, Grep, Glob # 只允许只读工具 + +// 建议:添加可选的工具限制 +Task({ + description: '查找测试文件', + prompt: '...', + tools: ['Read', 'Grep', 'Glob'] // 🆕 可选工具限制 +}) +``` + +### 2. **缺少自动委托** +```typescript +// 当前实现:需要手动调用 Task 工具 +// LLM 需要主动决定使用 Task + +// Claude Code:自动委托 +// 当检测到 "review code" 时自动调用 code-reviewer subagent + +// 建议:添加触发器机制 +export const taskTool = createTool({ + // ... + triggers: ['分析代码', '查找文件', '生成文档'], // 🆕 触发词 +}) +``` + +### 3. **缺少持久化配置** +```typescript +// 当前实现:每次运行时动态生成 + +// Claude Code:可以保存和复用 subagent 配置 +.claude/agents/my-analyzer.md + +// 建议:添加可选的配置保存 +Task({ + description: '分析代码', + prompt: '...', + save: 'code-analyzer' // 🆕 保存配置以便复用 +}) +``` + +### 4. **缺少并行执行** +```typescript +// 当前实现:一次只能执行一个任务 + +// Cursor 2.0:可以并行执行 8 个 agent + +// 建议:添加批量执行 +TaskBatch([ + { description: '分析前端', prompt: '...' }, + { description: '分析后端', prompt: '...' }, + { description: '分析数据库', prompt: '...' } +]) // 🆕 并行执行多个任务 +``` + +### 5. **缺少会话恢复** +```typescript +// 当前实现:每次创建新的子 Agent + +// Claude Code:可以恢复之前的会话 +// agentId 保存在 agent-{agentId}.jsonl + +// 建议:添加会话 ID +Task({ + description: '继续分析', + prompt: '...', + resumeFrom: 'agent-abc123' // 🆕 恢复之前的会话 +}) +``` + +--- + +## 🎯 改进建议 + +### 优先级 1:工具隔离(安全性) + +**理由**:防止子 Agent 执行危险操作(如删除文件、执行系统命令) + +**实现**: +```typescript +schema: z.object({ + description: z.string().min(3).max(100), + prompt: z.string().min(10), + model: z.enum(['haiku', 'sonnet', 'opus']).optional(), + tools: z.array(z.string()).optional(), // 🆕 工具白名单 +}) + +// 执行时过滤工具 +const allowedTools = params.tools || ['*']; +const subAgent = await agentFactory(dynamicSystemPrompt, allowedTools); +``` + +**示例**: +```typescript +Task({ + description: '查找测试文件', + prompt: '查找所有 .test.ts 文件', + tools: ['Read', 'Grep', 'Glob'] // 只允许只读工具 +}) +``` + +--- + +### 优先级 2:自动委托(便利性) + +**理由**:减少手动调用,让 LLM 更主动使用 Task 工具 + +**实现**: +```typescript +description: { + short: '...', + long: ` +... +**触发场景**: +- 当需要深入分析代码时 +- 当需要搜索大量文件时 +- 当任务需要多个步骤时 +**自动委托**:包含关键词(分析、搜索、生成文档)时自动推荐使用 + ` +} +``` + +--- + +### 优先级 3:可选配置保存(可复用性) + +**理由**:常用任务可以保存为模板,避免重复编写 + +**实现**: +```typescript +// 保存配置 +Task({ + description: '代码质量分析', + prompt: '分析代码质量,检查...', + save: 'code-quality-analyzer' +}) + +// 复用配置 +Task({ + use: 'code-quality-analyzer', + prompt: '分析 src/components/ 目录' // 覆盖原 prompt +}) +``` + +**存储位置**: +``` +.blade/ + tasks/ + code-quality-analyzer.json +``` + +--- + +### 优先级 4:并行执行(性能) + +**理由**:提升大型任务执行效率 + +**实现**: +```typescript +export const taskBatchTool = createTool({ + name: 'TaskBatch', + schema: z.object({ + tasks: z.array(z.object({ + description: z.string(), + prompt: z.string(), + model: z.enum(['haiku', 'sonnet', 'opus']).optional(), + })), + maxParallel: z.number().default(3), + }), + + async execute(params, context) { + const results = await Promise.allSettled( + params.tasks.map(task => + executeTask(task, context) + ) + ); + return aggregateResults(results); + } +}) +``` + +--- + +### 优先级 5:会话恢复(长期任务) + +**理由**:支持长期运行的分析任务 + +**实现**: +```typescript +Task({ + description: '继续代码审查', + resumeFrom: sessionId, // 从历史会话恢复 + prompt: '继续审查剩余文件' +}) +``` + +--- + +## 📊 最终评估 + +### Blade 当前实现的定位 + +**强项**: +1. ✅ **最简洁的 API**:2 个必需参数,比 Claude Code 简单 50% +2. ✅ **最灵活的设计**:动态生成,无需配置文件 +3. ✅ **模型主导**:完全信任 LLM 的决策能力 +4. ✅ **代码最少**:293 行 vs Claude Code ~500 行 + +**弱项**: +1. ❌ 缺少工具隔离(安全风险) +2. ❌ 缺少自动委托(需手动调用) +3. ❌ 缺少并行执行(性能受限) +4. ❌ 缺少会话恢复(长期任务不便) + +### 竞争力排名 + +**简洁性**:Blade > OpenCode > Claude Code > Cursor +**安全性**:Claude Code > OpenCode > Blade > Cursor +**性能**:Cursor >> Blade ≈ Claude Code ≈ OpenCode +**灵活性**:Blade > OpenCode > Claude Code > Cursor + +### 建议的演进路径 + +**Phase 1(立即)**: +- ✅ 保持当前简洁设计 +- ✅ 添加工具隔离(`tools` 参数) +- ✅ 改进工具描述(添加触发词提示) + +**Phase 2(短期)**: +- 🔲 添加可选配置保存(`.blade/tasks/`) +- 🔲 添加批量执行工具(`TaskBatch`) +- 🔲 改进错误处理和超时控制 + +**Phase 3(长期)**: +- 🔲 添加会话恢复机制 +- 🔲 添加并行执行优化 +- 🔲 添加资源监控和限制 + +--- + +## 🎉 结论 + +**Blade 的 Task 工具实现是正确的方向**: + +1. **设计哲学正确**: + - Claude Code 的方向是对的(简洁、模型决策) + - 但 Claude Code 仍然有配置文件的复杂性 + - Blade 更进一步,完全去掉配置文件 ✅ + +2. **实现质量高**: + - 代码简洁(293 行) + - 类型安全(Zod 验证) + - 错误处理完善 + +3. **改进空间明确**: + - 优先添加工具隔离(安全性) + - 其他功能可以根据实际需求逐步添加 + +4. **竞争优势**: + - 比 Claude Code 更简洁 + - 比 OpenCode 更稳定 + - 比 Cursor 更轻量 + +**最重要的是**:Blade 的设计是面向未来的——相信模型的能力,让它自己决定策略,而不是用复杂的配置系统限制它。 diff --git a/docs/development/task-tool-improvements.md b/docs/development/task-tool-improvements.md new file mode 100644 index 00000000..de604c98 --- /dev/null +++ b/docs/development/task-tool-improvements.md @@ -0,0 +1,509 @@ +# Task 工具改进:工具隔离 + 自动委托 + +> 实现日期:2025-11-14 +> 基于竞品分析的优先级改进 + +## 📋 改进概述 + +根据对 **Claude Code**、**OpenCode** 和 **Cursor 2.0** 的竞品分析,我们实现了两个优先级最高的改进: + +1. **工具隔离(安全性)** - 限制子 Agent 可用的工具列表 +2. **自动委托提示(便利性)** - 引导 LLM 主动使用 Task 工具 + +--- + +## 🔒 改进 1:工具隔离(安全性) + +### 问题 + +**之前**:子 Agent 继承所有工具,可能执行危险操作(如删除文件、执行系统命令) + +```typescript +// 旧实现:子 Agent 继承所有工具 +const subAgent = await agentFactory(dynamicSystemPrompt); +// 可以使用:Read, Write, Edit, Bash, WebSearch, WebFetch 等所有工具 +``` + +### 解决方案 + +**现在**:添加可选的 `tools` 参数,限制子 Agent 可用的工具列表 + +```typescript +// 新实现:可以限制工具访问 +Task({ + description: '查找测试文件', + prompt: '查找所有 .test.ts 文件', + tools: ['Read', 'Grep', 'Glob'] // 只允许只读工具 +}) +``` + +### 实现细节 + +#### 1. Schema 扩展 + +```typescript +// src/tools/builtin/task/task.ts:47-61 +schema: z.object({ + description: z.string().min(3).max(100), + prompt: z.string().min(10), + model: z.enum(['haiku', 'sonnet', 'opus']).optional(), + tools: z.array(z.string()).optional() // 🆕 工具白名单 + .describe('允许使用的工具列表(可选,默认允许所有工具)') +}), +``` + +#### 2. Agent Factory 签名更新 + +```typescript +// src/tools/builtin/task/task.ts:18-31 +let agentFactory: + | ((systemPrompt?: string, allowedTools?: string[]) => Promise) + | undefined; + +export function setTaskToolAgentFactory( + factory: (systemPrompt?: string, allowedTools?: string[]) => Promise +): void { + agentFactory = factory; +} +``` + +#### 3. 工具过滤实现 + +```typescript +// src/ui/hooks/useCommandHandler.ts:148-160 +if (allowedTools && allowedTools.length > 0) { + const toolRegistry = agent.getToolRegistry(); + const allTools = toolRegistry.getAll(); + + // 禁用不在允许列表中的工具 + for (const tool of allTools) { + if (!allowedTools.includes(tool.name)) { + toolRegistry.unregister(tool.name); + } + } +} +``` + +#### 4. Agent API 扩展 + +```typescript +// src/agent/Agent.ts:1086-1091 +/** + * 获取工具注册表(用于子 Agent 工具隔离) + */ +public getToolRegistry(): ToolRegistry { + return this.executionPipeline.getRegistry(); +} +``` + +#### 5. 动态系统提示词生成 + +```typescript +// src/tools/builtin/task/task.ts:278-361 +function buildDynamicSystemPrompt( + taskPrompt: string, + model: string, + allowedTools?: string[] +): string { + // 工具限制说明 + if (allowedTools && allowedTools.length > 0) { + basePrompt += `⚠️ **工具访问限制**:你只能使用以下工具:${allowedTools.join(', ')}`; + + // 只列出允许的工具 + for (const tool of allowedTools) { + basePrompt += `- **${tool}**: ${allTools[tool]}\n`; + } + + // 添加严格遵守提示 + basePrompt += `5. **严格遵守工具限制**: 不要尝试使用未授权的工具\n`; + } + + return basePrompt; +} +``` + +### 使用示例 + +#### 示例 1:只读权限(安全搜索) + +```typescript +Task({ + description: '查找测试文件', + prompt: '查找项目中所有的测试文件(.test.ts, .spec.ts)', + tools: ['Read', 'Grep', 'Glob'] // 只允许只读工具 +}) +``` + +#### 示例 2:读写权限(文档生成) + +```typescript +Task({ + description: '生成 API 文档', + prompt: '分析 src/api/ 目录下的所有 API 路由,生成完整的 API 文档', + tools: ['Read', 'Grep', 'Glob', 'Write'] // 允许读取和写入,但不允许执行命令 +}) +``` + +#### 示例 3:完全权限(依赖分析) + +```typescript +Task({ + description: '分析项目依赖', + prompt: '分析项目中的所有依赖包,检查过时的包和安全漏洞', + // 不指定 tools,允许所有工具 +}) +``` + +### 安全最佳实践 + +| 任务类型 | 推荐工具 | 原因 | +|---------|---------|------| +| 文件搜索 | `Read, Grep, Glob` | 只读操作,安全 | +| 代码分析 | `Read, Grep, Glob` | 只读操作,安全 | +| 文档生成 | `Read, Grep, Glob, Write` | 允许写入,但不执行命令 | +| 依赖检查 | `Read, Bash` | 需要执行 npm 命令 | +| 完整任务 | 不指定(默认所有) | 信任模型决策 | + +--- + +## 🔥 改进 2:自动委托提示(便利性) + +### 问题 + +**之前**:LLM 需要主动决定使用 Task 工具,可能错过适用场景 + +```typescript +// 旧描述:简单说明功能 +description: { + short: '启动独立的 AI 助手执行任务', + long: '启动独立的 AI 助手来处理复杂任务...' +} +``` + +### 解决方案 + +**现在**:在工具描述中添加明确的触发场景和 "Use PROACTIVELY" 提示 + +```typescript +// 新描述:明确触发场景 + 强烈建议主动使用 +**🔥 自动委托提示(Use PROACTIVELY):** +当遇到以下场景时,**强烈建议**主动使用此工具: +- 需要**深入分析代码结构**或架构设计 +- 需要**搜索大量文件**或执行复杂的代码搜索 +- 需要**生成文档、报告或总结** +- 需要**多步骤推理**或执行复杂的工作流 +- 任务可以**独立完成**,不需要与用户频繁交互 +``` + +### 实现细节 + +#### 1. 工具描述增强 + +```typescript +// src/tools/builtin/task/task.ts:63-95 +description: { + short: '启动独立的 AI 助手自主执行复杂的多步骤任务', + long: ` +启动独立的 AI 助手来处理复杂任务。助手会自动选择合适的工具和策略来完成任务。 + +**🔥 自动委托提示(Use PROACTIVELY):** +当遇到以下场景时,**强烈建议**主动使用此工具: +- 需要**深入分析代码结构**或架构设计 +- 需要**搜索大量文件**或执行复杂的代码搜索 +- 需要**生成文档、报告或总结** +- 需要**多步骤推理**或执行复杂的工作流 +- 任务可以**独立完成**,不需要与用户频繁交互 + +**适用场景:** +- 代码分析:分析项目依赖、检查代码质量、查找潜在问题 +- 文件搜索:查找测试文件、配置文件、特定模式的代码 +- 文档生成:生成 API 文档、README、技术报告 +- 重构建议:分析代码并提供重构方案 +- 问题诊断:调查 bug、分析日志、查找错误原因 + +**助手的能力:** +- 自动选择和使用工具(Read、Write、Grep、Glob、Bash、WebSearch 等) +- 自主决定执行策略和步骤 +- 独立的执行上下文(不共享父 Agent 的对话历史) +- 可限制工具访问(通过 tools 参数提升安全性) + +**⚠️ 重要:** +- 这不是 TODO 清单管理工具(使用 TodoWrite 管理任务清单) +- prompt 应该包含完整的上下文和详细的期望输出 +- 助手会消耗独立的 API token +- 对于敏感操作,可通过 tools 参数限制工具使用(如只允许只读工具) + `.trim(), +} +``` + +#### 2. 使用说明更新 + +```typescript +// src/tools/builtin/task/task.ts:96-103 +usageNotes: [ + 'description 应简短(3-10个词),如"分析项目依赖"', + 'prompt 应详细完整,包含任务目标、期望输出格式', + '助手无法访问父 Agent 的对话历史,需在 prompt 中提供完整上下文', + '助手会自动选择合适的工具,无需指定(除非使用 tools 参数限制)', + 'model 参数可选:haiku(快速)、sonnet(平衡)、opus(高质量)', + 'tools 参数可选:限制可用工具列表,提升安全性(如:["Read", "Grep", "Glob"])', +] +``` + +#### 3. 重要提示增强 + +```typescript +// src/tools/builtin/task/task.ts:133-140 +important: [ + '⚠️ 这不是 TODO 清单工具!管理任务清单请使用 TodoWrite', + '🔥 当需要深入分析、大量搜索、生成文档时,主动使用此工具(PROACTIVELY)', + '助手会消耗独立的 API token', + '助手无法访问父 Agent 的对话历史', + 'prompt 应该详细完整,包含所有必要的上下文', + '🔒 对于敏感操作,使用 tools 参数限制工具访问(安全最佳实践)', +] +``` + +#### 4. 示例更新 + +```typescript +// src/tools/builtin/task/task.ts:104-132 +examples: [ + { + description: '分析项目依赖(完全权限)', + params: { + description: '分析项目依赖', + prompt: '分析项目中的所有依赖包...', + }, + }, + { + description: '查找测试文件(只读权限)', + params: { + description: '查找测试文件', + prompt: '查找项目中所有的测试文件...', + tools: ['Read', 'Grep', 'Glob'], // 只允许只读工具 + }, + }, + { + description: '生成 API 文档(高质量模型)', + params: { + description: '生成 API 文档', + prompt: '分析 src/api/ 目录下的所有 API 路由...', + model: 'opus', + tools: ['Read', 'Grep', 'Glob', 'Write'], // 允许读写,但不允许执行命令 + }, + }, +] +``` + +### 触发场景 + +LLM 应该在以下情况下**主动使用** Task 工具: + +| 场景 | 示例用户请求 | 为什么适合 Task | +|-----|-------------|----------------| +| 代码分析 | "分析项目依赖" | 需要读取多个文件,检查版本 | +| 文件搜索 | "查找所有测试文件" | 需要 Glob/Grep 多次搜索 | +| 文档生成 | "生成 API 文档" | 需要读取代码、分析、写入文档 | +| 架构分析 | "这个项目的架构是什么" | 需要探索多个目录和文件 | +| 问题诊断 | "为什么构建失败" | 需要查看日志、配置、代码 | + +--- + +## 📊 改进对比 + +### 参数对比 + +| 参数 | 旧版本 | 新版本 | 说明 | +|-----|-------|-------|------| +| `description` | ✅ | ✅ | 任务简短描述 | +| `prompt` | ✅ | ✅ | 详细任务指令 | +| `model` | ✅ | ✅ | 模型选择 | +| `tools` | ❌ | ✅ | 🆕 工具白名单(安全) | + +### 功能对比 + +| 功能 | 旧版本 | 新版本 | 改进 | +|-----|-------|-------|------| +| 工具隔离 | ❌ | ✅ | 提升安全性 | +| 自动委托提示 | ❌ | ✅ | 提升便利性 | +| 动态提示词 | ✅ | ✅ | 保持 | +| 模型选择 | ✅ | ✅ | 保持 | + +### 与竞品对比 + +| 特性 | Claude Code | OpenCode | Blade (新版) | +|-----|------------|----------|-------------| +| 工具隔离 | ✅ tools 字段 | ✅ tools 布尔映射 | ✅ tools 数组 | +| 自动委托 | ✅ PROACTIVELY | ✅ mode 系统 | ✅ 场景提示 | +| 配置方式 | Markdown 文件 | Markdown 文件 | 代码参数 ✅ | +| 动态提示词 | ❌ 硬编码 | ❌ 固定 | ✅ 动态生成 | + +--- + +## 🎯 使用指南 + +### 场景 1:安全的文件搜索 + +```typescript +// 任务:查找所有配置文件 +// 风险:低(只读操作) +// 推荐:限制工具为只读 + +Task({ + description: '查找配置文件', + prompt: '查找项目中所有的配置文件(.json, .yaml, .toml),列出路径和用途', + tools: ['Read', 'Grep', 'Glob'] // 只读工具 +}) +``` + +### 场景 2:文档生成 + +```typescript +// 任务:生成完整的 API 文档 +// 风险:中(需要写入文件) +// 推荐:允许读写,但不允许执行命令 + +Task({ + description: '生成 API 文档', + prompt: ` +分析 src/api/ 目录下的所有 API 路由,生成完整的 API 文档。 + +要求: +1. 分析所有路由文件 +2. 提取路由、请求参数、响应格式 +3. 生成 Markdown 格式的文档 +4. 保存到 docs/api.md + `, + model: 'opus', // 使用高质量模型 + tools: ['Read', 'Grep', 'Glob', 'Write'] // 允许读写 +}) +``` + +### 场景 3:完整的依赖分析 + +```typescript +// 任务:分析项目依赖并检查更新 +// 风险:中(需要执行 npm 命令) +// 推荐:允许所有工具(信任模型) + +Task({ + description: '分析项目依赖', + prompt: ` +分析项目依赖并生成报告: + +1. 读取 package.json +2. 使用 npm outdated 检查过时的包 +3. 使用 npm audit 检查安全漏洞 +4. 生成 Markdown 报告,包含: + - 过时的包及最新版本 + - 安全漏洞及修复建议 + - 升级优先级排序 + `, + model: 'sonnet' + // 不指定 tools,允许使用所有工具 +}) +``` + +--- + +## 🔍 技术细节 + +### 工具过滤实现 + +```typescript +// 1. 创建 Agent +const agent = await Agent.create({ + systemPrompt: dynamicSystemPrompt, + appendSystemPrompt: appendSystemPrompt, + maxTurns: maxTurns, +}); + +// 2. 如果指定了工具限制,则过滤 +if (allowedTools && allowedTools.length > 0) { + const toolRegistry = agent.getToolRegistry(); + const allTools = toolRegistry.getAll(); + + // 3. 禁用不在白名单中的工具 + for (const tool of allTools) { + if (!allowedTools.includes(tool.name)) { + toolRegistry.unregister(tool.name); + } + } +} + +return agent; +``` + +### 系统提示词生成 + +```typescript +// 根据工具限制生成不同的提示词 +if (allowedTools && allowedTools.length > 0) { + basePrompt += `⚠️ **工具访问限制**:你只能使用以下工具:${allowedTools.join(', ')}`; + + // 只列出允许的工具 + for (const tool of allowedTools) { + if (allTools[tool]) { + basePrompt += `- **${tool}**: ${allTools[tool]}\n`; + } + } + + // 添加严格遵守提示 + basePrompt += `5. **严格遵守工具限制**: 不要尝试使用未授权的工具\n`; +} +``` + +--- + +## ✅ 验证清单 + +### 功能验证 + +- [x] `tools` 参数可选,默认允许所有工具 +- [x] 指定 `tools` 后,子 Agent 只能使用指定工具 +- [x] 系统提示词正确反映工具限制 +- [x] 工具描述包含 "Use PROACTIVELY" 提示 +- [x] 示例展示不同的工具限制场景 + +### 安全验证 + +- [x] 只读工具:`Read, Grep, Glob` +- [x] 读写工具:`Read, Grep, Glob, Write, Edit` +- [x] 执行工具:`Bash` +- [x] 网络工具:`WebSearch, WebFetch` + +### 构建验证 + +```bash +npm run build +# ✅ Bundled 1450 modules in 213ms +# ✅ blade.js 6.92 MB +``` + +--- + +## 📚 相关文档 + +- [竞品分析](./task-tool-competitive-analysis.md) - 详细的竞品对比 +- [V2 重构文档](./task-tool-refactor-v2.md) - 简洁设计哲学 +- [Agent 无状态设计](./agent-stateless-design.md) - Agent 架构 + +--- + +## 🎉 总结 + +通过这两个改进,Blade 的 Task 工具现在具备: + +1. **✅ 安全性**:通过工具隔离防止子 Agent 执行危险操作 +2. **✅ 便利性**:通过明确的触发场景引导 LLM 主动使用 +3. **✅ 灵活性**:保持简洁的 API,工具限制为可选参数 +4. **✅ 竞争力**:在安全性和便利性上达到竞品水平 + +同时保持了 Blade 的核心优势: + +- **极简 API**:只有 2 个必需参数 +- **动态提示词**:运行时生成,无需配置文件 +- **模型决策**:信任模型的能力,最小化限制 + +这些改进使 Blade 在保持简洁设计的同时,提升了安全性和可用性! diff --git a/package.json b/package.json index 8c4024bb..af00b1a8 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "vitest": "^3.0.0" }, "dependencies": { + "@inkjs/ui": "^2.0.0", "@modelcontextprotocol/sdk": "^1.17.4", "ahooks": "^3.9.5", "axios": "^1.12.2", @@ -114,7 +115,6 @@ "ink": "^6.4.0", "ink-big-text": "^2.0.0", "ink-gradient": "^3.0.0", - "ink-multi-select": "^2.0.0", "ink-progress-bar": "^3.0.0", "ink-select-input": "^6.2.0", "ink-spinner": "^5.0.0", @@ -130,6 +130,7 @@ "react-dom": "^19.1.1", "string-width": "^8.1.0", "ws": "^8.18.0", + "yaml": "^2.8.1", "yargs": "^18.0.0", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80bb5d2b..df6a2832 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@inkjs/ui': + specifier: ^2.0.0 + version: 2.0.0(ink@6.4.0(@types/react@19.2.2)(react@19.2.0)) '@modelcontextprotocol/sdk': specifier: ^1.17.4 version: 1.21.1 @@ -38,9 +41,6 @@ importers: ink-gradient: specifier: ^3.0.0 version: 3.0.0(ink@6.4.0(@types/react@19.2.2)(react@19.2.0)) - ink-multi-select: - specifier: ^2.0.0 - version: 2.0.0 ink-progress-bar: specifier: ^3.0.0 version: 3.0.0 @@ -86,6 +86,9 @@ importers: ws: specifier: ^8.18.0 version: 8.18.3 + yaml: + specifier: ^2.8.1 + version: 2.8.1 yargs: specifier: ^18.0.0 version: 18.0.0 @@ -140,7 +143,7 @@ importers: version: 17.0.34 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/node@22.19.0)(jsdom@26.1.0)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.0)(jsdom@26.1.0)(yaml@2.8.1)) execa: specifier: ^9.6.0 version: 9.6.0 @@ -155,7 +158,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@22.19.0)(jsdom@26.1.0) + version: 3.2.4(@types/node@22.19.0)(jsdom@26.1.0)(yaml@2.8.1) packages: @@ -221,28 +224,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@2.3.4': resolution: {integrity: sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.4': resolution: {integrity: sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@2.3.4': resolution: {integrity: sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@2.3.4': resolution: {integrity: sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw==} @@ -444,6 +443,12 @@ packages: cpu: [x64] os: [win32] + '@inkjs/ui@2.0.0': + resolution: {integrity: sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg==} + engines: {node: '>=18'} + peerDependencies: + ink: '>=5' + '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} @@ -657,67 +662,56 @@ packages: resolution: {integrity: sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.1': resolution: {integrity: sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.1': resolution: {integrity: sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.1': resolution: {integrity: sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.1': resolution: {integrity: sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.53.1': resolution: {integrity: sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.53.1': resolution: {integrity: sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.1': resolution: {integrity: sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.1': resolution: {integrity: sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.1': resolution: {integrity: sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.1': resolution: {integrity: sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.53.1': resolution: {integrity: sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==} @@ -962,10 +956,6 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - arr-rotate@1.0.0: - resolution: {integrity: sha512-yOzOZcR9Tn7enTF66bqKorGGH0F36vcPaSWg8fO0c0UYb3LX3VMXj5ZxEqQLNOecAhlRJ7wYZja5i4jTlnbIfQ==} - engines: {node: '>=4'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1051,6 +1041,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-spinners@3.3.0: + resolution: {integrity: sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==} + engines: {node: '>=18.20'} + cli-truncate@4.0.0: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} @@ -1139,6 +1133,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-property@1.0.0: resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} engines: {node: '>=0.10.0'} @@ -1234,10 +1232,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -1290,10 +1284,6 @@ packages: picomatch: optional: true - figures@2.0.0: - resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} - engines: {node: '>=4'} - figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -1447,10 +1437,6 @@ packages: peerDependencies: ink: '>=4' - ink-multi-select@2.0.0: - resolution: {integrity: sha512-IK1oTucJ3gy/2M3xXicuTJCuCLSeQzdAf+iJV7eyfxYG4kCSO9W2TSV02TVfz+AKS8/8r7X/ssTlDRmAXjLZHQ==} - engines: {node: '>=8'} - ink-progress-bar@3.0.0: resolution: {integrity: sha512-GzByB3uEofqjyWC3VmdhYpBq+kzszu5Nwt/NruTDWa7fbw1E6sx6U1n6Kcsfj9D3qwR17dtC5w9uFVMyRA5HZw==} @@ -1617,10 +1603,6 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -2307,6 +2289,11 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -2518,6 +2505,14 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@inkjs/ui@2.0.0(ink@6.4.0(@types/react@19.2.2)(react@19.2.0))': + dependencies: + chalk: 5.6.2 + cli-spinners: 3.3.0 + deepmerge: 4.3.1 + figures: 6.1.0 + ink: 6.4.0(@types/react@19.2.2)(react@19.2.0) + '@inquirer/ansi@1.0.2': {} '@inquirer/checkbox@4.3.1(@types/node@22.19.0)': @@ -2875,7 +2870,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.0)(jsdom@26.1.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.0)(jsdom@26.1.0)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -2890,7 +2885,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.19.0)(jsdom@26.1.0) + vitest: 3.2.4(@types/node@22.19.0)(jsdom@26.1.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2902,13 +2897,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.2.2(@types/node@22.19.0))': + '@vitest/mocker@3.2.4(vite@7.2.2(@types/node@22.19.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.2(@types/node@22.19.0) + vite: 7.2.2(@types/node@22.19.0)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2997,8 +2992,6 @@ snapshots: dependencies: dequal: 2.0.3 - arr-rotate@1.0.0: {} - assertion-error@2.0.1: {} ast-v8-to-istanbul@0.3.8: @@ -3089,6 +3082,8 @@ snapshots: cli-spinners@2.9.2: {} + cli-spinners@3.3.0: {} + cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 @@ -3163,6 +3158,8 @@ snapshots: deep-eql@5.0.2: {} + deepmerge@4.3.1: {} + define-property@1.0.0: dependencies: is-descriptor: 1.0.3 @@ -3257,8 +3254,6 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} estree-walker@3.0.3: @@ -3334,10 +3329,6 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - figures@2.0.0: - dependencies: - escape-string-regexp: 1.0.5 - figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -3502,13 +3493,6 @@ snapshots: prop-types: 15.8.1 strip-ansi: 7.1.2 - ink-multi-select@2.0.0: - dependencies: - arr-rotate: 1.0.0 - figures: 2.0.0 - lodash.isequal: 4.5.0 - prop-types: 15.8.1 - ink-progress-bar@3.0.0: dependencies: blacklist: 1.1.4 @@ -3699,8 +3683,6 @@ snapshots: lodash-es@4.17.21: {} - lodash.isequal@4.5.0: {} - lodash@4.17.21: {} loose-envify@1.4.0: @@ -4200,13 +4182,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@22.19.0): + vite-node@3.2.4(@types/node@22.19.0)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.2(@types/node@22.19.0) + vite: 7.2.2(@types/node@22.19.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4221,7 +4203,7 @@ snapshots: - tsx - yaml - vite@7.2.2(@types/node@22.19.0): + vite@7.2.2(@types/node@22.19.0)(yaml@2.8.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -4232,12 +4214,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 fsevents: 2.3.3 + yaml: 2.8.1 - vitest@3.2.4(@types/node@22.19.0)(jsdom@26.1.0): + vitest@3.2.4(@types/node@22.19.0)(jsdom@26.1.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.2.2(@types/node@22.19.0)) + '@vitest/mocker': 3.2.4(vite@7.2.2(@types/node@22.19.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4255,8 +4238,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.2(@types/node@22.19.0) - vite-node: 3.2.4(@types/node@22.19.0) + vite: 7.2.2(@types/node@22.19.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.19.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.0 @@ -4344,6 +4327,8 @@ snapshots: y18n@5.0.8: {} + yaml@2.8.1: {} + yargs-parser@22.0.0: {} yargs@18.0.0: diff --git a/src/agent/Agent.ts b/src/agent/Agent.ts index a834a2d5..26370495 100644 --- a/src/agent/Agent.ts +++ b/src/agent/Agent.ts @@ -24,16 +24,16 @@ import { createLogger, LogCategory } from '../logging/Logger.js'; import { loadProjectMcpConfig } from '../mcp/loadProjectMcpConfig.js'; import { McpRegistry } from '../mcp/McpRegistry.js'; import { - createPlanModeReminder, - PLAN_MODE_SYSTEM_PROMPT, - PromptBuilder, + createPlanModeReminder, + PLAN_MODE_SYSTEM_PROMPT, + PromptBuilder, } from '../prompts/index.js'; import { AttachmentCollector } from '../prompts/processors/AttachmentCollector.js'; import type { Attachment } from '../prompts/processors/types.js'; import { - createChatService, - type IChatService, - type Message, + createChatService, + type IChatService, + type Message, } from '../services/ChatServiceInterface.js'; import { getBuiltinTools } from '../tools/builtin/index.js'; import { ExecutionPipeline } from '../tools/execution/ExecutionPipeline.js'; @@ -42,16 +42,17 @@ import type { Tool, ToolResult } from '../tools/types/index.js'; import { getEnvironmentContext } from '../utils/environment.js'; import { ExecutionEngine } from './ExecutionEngine.js'; import { - type LoopDetectionConfig, - LoopDetectionService, + type LoopDetectionConfig, + LoopDetectionService, } from './LoopDetectionService.js'; +import { subagentRegistry } from './subagents/SubagentRegistry.js'; import type { - AgentOptions, - AgentResponse, - AgentTask, - ChatContext, - LoopOptions, - LoopResult, + AgentOptions, + AgentResponse, + AgentTask, + ChatContext, + LoopOptions, + LoopResult, } from './types.js'; // 创建 Agent 专用 Logger @@ -138,6 +139,12 @@ export class Agent extends EventEmitter { // 将 options 作为运行时参数传递 const agent = new Agent(config, options); await agent.initialize(); + + // 7. 应用工具白名单(如果指定) + if (options.toolWhitelist && options.toolWhitelist.length > 0) { + agent.applyToolWhitelist(options.toolWhitelist); + } + return agent; } @@ -158,7 +165,10 @@ export class Agent extends EventEmitter { // 2. 注册内置工具 await this.registerBuiltinTools(); - // 3. 初始化核心组件 + // 3. 加载 subagent 配置 + await this.loadSubagents(); + + // 4. 初始化核心组件 // 获取当前模型配置 const configManager = ConfigManager.getInstance(); const modelConfig = configManager.getCurrentModel(); @@ -487,6 +497,7 @@ export class Agent extends EventEmitter { let turnsCount = 0; const allToolResults: ToolResult[] = []; + let totalTokens = 0; // 累计 token 使用量 while (turnsCount < maxTurns) { // === 1. 检查中断信号 === @@ -560,6 +571,11 @@ export class Agent extends EventEmitter { // 3. 直接调用 ChatService(OpenAI SDK 已内置重试机制) const turnResult = await this.chatService.chat(messages, tools, options?.signal); + // 累加 token 使用量 + if (turnResult.usage?.totalTokens) { + totalTokens += turnResult.usage.totalTokens; + } + // 检查 abort 信号(LLM 调用后) if (options?.signal?.aborted) { return { @@ -614,6 +630,7 @@ export class Agent extends EventEmitter { turnsCount, toolCallsCount: allToolResults.length, duration: Date.now() - startTime, + tokensUsed: totalTokens, }, }; } @@ -1083,6 +1100,32 @@ export class Agent extends EventEmitter { return this.executionPipeline ? this.executionPipeline.getRegistry().getAll() : []; } + /** + * 获取工具注册表(用于子 Agent 工具隔离) + */ + public getToolRegistry(): ToolRegistry { + return this.executionPipeline.getRegistry(); + } + + /** + * 应用工具白名单(仅保留指定工具) + */ + public applyToolWhitelist(whitelist: string[]): void { + const registry = this.executionPipeline.getRegistry(); + const allTools = registry.getAll(); + + // 过滤掉不在白名单中的工具 + const toolsToRemove = allTools.filter((tool) => !whitelist.includes(tool.name)); + + for (const tool of toolsToRemove) { + registry.unregister(tool.name); + } + + logger.debug( + `🔒 Applied tool whitelist: ${whitelist.join(', ')} (removed ${toolsToRemove.length} tools)` + ); + } + /** * 获取工具统计信息 */ @@ -1295,22 +1338,6 @@ export class Agent extends EventEmitter { }); logger.debug(`📦 Registering ${builtinTools.length} builtin tools...`); - // 为 TaskTool 注入 agentFactory(支持子任务递归) - const taskTool = builtinTools.find((t) => t.name === 'task'); - if ( - taskTool && - 'setAgentFactory' in taskTool && - typeof taskTool.setAgentFactory === 'function' - ) { - logger.debug('🔧 Injecting agentFactory into TaskTool...'); - taskTool.setAgentFactory(async () => { - // 创建新的子 Agent 实例(使用默认 pipeline) - const subAgent = new Agent(this.config, {}); - await subAgent.initialize(); - return subAgent; - }); - } - this.executionPipeline.getRegistry().registerAll(builtinTools); const registeredCount = this.executionPipeline.getRegistry().getAll().length; @@ -1387,6 +1414,29 @@ export class Agent extends EventEmitter { } } + /** + * 加载 subagent 配置 + */ + private async loadSubagents(): Promise { + // 如果已经加载过,跳过(全局单例,只需加载一次) + if (subagentRegistry.getAllNames().length > 0) { + logger.debug(`📦 Subagents already loaded: ${subagentRegistry.getAllNames().join(', ')}`); + return; + } + + try { + const loadedCount = subagentRegistry.loadFromStandardLocations(); + if (loadedCount > 0) { + logger.debug(`✅ Loaded ${loadedCount} subagents: ${subagentRegistry.getAllNames().join(', ')}`); + } else { + logger.debug('📦 No subagents configured'); + } + } catch (error) { + logger.warn('Failed to load subagents:', error); + // 不抛出错误,允许 Agent 继续初始化 + } + } + /** * 处理 @ 文件提及 * 从用户消息中提取 @ 提及,读取文件内容,并追加到消息 diff --git a/src/agent/EnhancedSteeringController.ts b/src/agent/EnhancedSteeringController.ts deleted file mode 100644 index e9c2cb0b..00000000 --- a/src/agent/EnhancedSteeringController.ts +++ /dev/null @@ -1,603 +0,0 @@ -/** - * 增强型Steering控制器 - * 实现实时任务控制和动态重定向功能 - */ - -import { EventEmitter } from 'events'; -import type { AgentTask } from './types.js'; - -export interface SteeringDirection { - type: 'pause' | 'resume' | 'redirect' | 'cancel' | 'priority_adjust'; - targetTaskId?: string; - newTask?: AgentTask; - priority?: number; - reason?: string; - metadata?: Record; -} - -export interface TaskRedirectResult { - success: boolean; - originalTask: AgentTask; - newTask?: AgentTask; - redirectTime: number; - error?: string; -} - -export interface MessageEvent { - id: string; - type: 'steering' | 'status' | 'error'; - payload: unknown; - timestamp: number; - priority: 'high' | 'medium' | 'low'; -} - -export interface AsyncMessageQueue { - push(message: MessageEvent): void; - pop(): Promise; - peek(): MessageEvent | undefined; - size(): number; - clear(): void; -} - -export interface TaskInterceptor { - canIntercept(task: AgentTask): boolean; - intercept(task: AgentTask): Promise; -} - -/** - * 异步消息队列实现 - 支持优先级排序 - */ -class PriorityAsyncMessageQueue implements AsyncMessageQueue { - private messages: MessageEvent[] = []; - private resolvers: ((message: MessageEvent) => void)[] = []; - private readonly maxSize: number; - - constructor(maxSize = 1000) { - this.maxSize = maxSize; - } - - push(message: MessageEvent): void { - if (this.messages.length >= this.maxSize) { - this.messages.shift(); // 删除最早的消息 - } - - // 按优先级和时间排序插入 - const priorityValue = { high: 3, medium: 2, low: 1 }; - const insertIndex = this.messages.findIndex( - (msg) => priorityValue[msg.priority] < priorityValue[message.priority] - ); - - if (insertIndex === -1) { - this.messages.push(message); - } else { - this.messages.splice(insertIndex, 0, message); - } - - // 通知等待的接收方 - if (this.resolvers.length > 0) { - const resolver = this.resolvers.shift()!; - resolver(this.messages.shift()!); - } - } - - async pop(): Promise { - if (this.messages.length > 0) { - return this.messages.shift()!; - } - - // 等待新消息到来 - return new Promise((resolve) => { - this.resolvers.push(resolve); - }); - } - - peek(): MessageEvent | undefined { - return this.messages[0]; - } - - size(): number { - return this.messages.length; - } - - clear(): void { - this.messages.length = 0; - this.resolvers.length = 0; - } -} - -/** - * 默认任务拦截器 - */ -class DefaultTaskInterceptor implements TaskInterceptor { - private readonly interceptableTaskTypes: string[] = [ - 'code_generate', - 'code_review', - 'file_operation', - ]; - - canIntercept(task: AgentTask): boolean { - return this.interceptableTaskTypes.includes(task.type); - } - - async intercept(task: AgentTask): Promise { - // 检查任务是否可被拦截(例如:任务尚未执行关键操作) - if (!task.metadata || !('executionStage' in task.metadata)) { - return task; // 可以拦截 - } - - const stage = task.metadata.executionStage as string; - if (['preparing', 'validating'].includes(stage)) { - return task; // 可以拦截 - } - - return null; // 不可拦截 - } -} - -/** - * 增强型Steering控制器 - * 扩展标准SteeringController以支持实时控制和动态重定向 - */ -export class EnhancedSteeringController extends EventEmitter { - private readonly messageQueue: AsyncMessageQueue; - private readonly taskInterceptor: TaskInterceptor; - private isInitialized = false; - private isSteeringLoopActive = false; - private activeTasks = new Map(); - private taskHistory: AgentTask[] = []; - private steeringLoopInterval?: NodeJS.Timeout; - private readonly maxHistorySize = 100; - - constructor(messageQueue?: AsyncMessageQueue, taskInterceptor?: TaskInterceptor) { - super(); - this.messageQueue = messageQueue || new PriorityAsyncMessageQueue(); - this.taskInterceptor = taskInterceptor || new DefaultTaskInterceptor(); - } - - /** - * 初始化实时控制中心 - */ - public async initializeRealTimeControl(): Promise { - if (this.isInitialized) { - return; - } - - try { - this.log('初始化增强型Steering控制器...'); - - // 设置消息队列监听 - this.setupMessageQueue(); - - // 启用任务拦截机制 - this.enableTaskInterception(); - - // 启动实时控制循环 - this.startSteeringLoop(); - - this.isInitialized = true; - this.emit('initialized'); - this.log('增强型Steering控制器初始化完成'); - } catch (error) { - this.error('初始化失败', error as Error); - throw error; - } - } - - /** - * 设置消息队列 - */ - private setupMessageQueue(): void { - // 监听队列中的消息事件 - setImmediate(this.processMessageQueue.bind(this)); - } - - /** - * 消息队列处理循环 - */ - private async processMessageQueue(): Promise { - while (this.isInitialized) { - try { - const message = await this.messageQueue.pop(); - await this.handleMessage(message); - } catch (error) { - this.error('处理消息队列时出错', error as Error); - await new Promise((resolve) => setTimeout(resolve, 100)); // 短暂延迟后重试 - } - } - } - - /** - * 处理单个消息事件 - */ - private async handleMessage(message: MessageEvent): Promise { - switch (message.type) { - case 'steering': - await this.handleSteeringMessage( - message as MessageEvent & { payload: SteeringDirection } - ); - break; - case 'status': - this.handleStatusMessage(message); - break; - case 'error': - this.handleErrorMessage(message); - break; - default: - this.log(`未知消息类型: ${message.type}`); - } - } - - /** - * 启用任务拦截 - */ - private enableTaskInterception(): void { - this.log('任务拦截机制已启用'); - } - - /** - * 启动实时控制循环 - */ - private startSteeringLoop(): void { - if (this.isSteeringLoopActive) { - return; - } - - this.isSteeringLoopActive = true; - this.steeringLoopInterval = setInterval(() => { - this.executeSteeringLoopCycle(); - }, 50); // 每50ms检查一次,确保<100ms响应时间 - - this.log('实时控制循环已启动'); - } - - /** - * 执行控制循环周期 - */ - private executeSteeringLoopCycle(): void { - // 检查当前任务状态 - this.checkActiveTasks(); - - // 处理高优先级消息 - this.processHighPriorityMessages(); - } - - /** - * 检查活动任务状态 - */ - private checkActiveTasks(): void { - this.activeTasks.forEach((task, taskId) => { - if (task.metadata) { - const timeoutThreshold = 30000; // 30秒超时 - const startTime = Number(task.metadata.startTime || 0); - - if (Date.now() - startTime > timeoutThreshold) { - this.emit('taskTimeout', { taskId, task }); - this.activeTasks.delete(taskId); - } - } - }); - } - - /** - * 处理高优先级消息 - */ - private processHighPriorityMessages(): void { - while (this.messageQueue.size() > 0) { - const message = this.messageQueue.peek(); - if (!message || message.priority !== 'high') { - break; // 只处理高优先级消息 - } - - this.messageQueue.pop(); - // 已移出队列的消息将在下一个处理循环中处理 - } - } - - /** - * 处理Steering控制消息 - */ - private async handleSteeringMessage( - message: MessageEvent & { payload: SteeringDirection } - ): Promise { - const direction = message.payload; - - switch (direction.type) { - case 'pause': - await this.pauseTask(direction.targetTaskId); - break; - case 'resume': - await this.resumeTask(direction.targetTaskId); - break; - case 'redirect': - await this.redirectTask(direction.targetTaskId!, direction); - break; - case 'cancel': - await this.cancelTask(direction.targetTaskId); - break; - case 'priority_adjust': - await this.adjustTaskPriority(direction.targetTaskId!, direction.priority!); - break; - } - - this.emit('steeringExecuted', { direction, timestamp: Date.now() }); - } - - /** - * 暂停任务 - */ - private async pauseTask(taskId?: string): Promise { - if (!taskId) { - // 暂停所有任务 - this.activeTasks.forEach((_, id) => { - this.emit('taskPaused', { taskId: id }); - }); - } else { - if (this.activeTasks.has(taskId)) { - this.emit('taskPaused', { taskId }); - } - } - } - - /** - * 恢复任务 - */ - private async resumeTask(taskId?: string): Promise { - if (!taskId) { - // 恢复所有任务 - this.activeTasks.forEach((_, id) => { - this.emit('taskResumed', { taskId: id }); - }); - } else { - if (this.activeTasks.has(taskId)) { - this.emit('taskResumed', { taskId }); - } - } - } - - /** - * 取消任务 - */ - private async cancelTask(taskId?: string): Promise { - if (!taskId) { - // 取消所有任务 - const taskIds = Array.from(this.activeTasks.keys()); - taskIds.forEach((id) => { - this.activeTasks.delete(id); - this.emit('taskCancelled', { taskId: id }); - }); - } else { - if (this.activeTasks.has(taskId)) { - this.activeTasks.delete(taskId); - this.emit('taskCancelled', { taskId }); - } - } - } - - /** - * 调整任务优先级 - */ - private async adjustTaskPriority(taskId: string, newPriority: number): Promise { - const task = this.activeTasks.get(taskId); - if (task) { - task.priority = newPriority; - this.emit('taskPriorityAdjusted', { taskId, newPriority }); - } - } - - /** - * 拦截并重定向任务 - 核心功能 - */ - public async interceptAndRedirect( - currentTask: AgentTask, - newDirection: SteeringDirection - ): Promise { - const startTime = Date.now(); - - try { - // 检查是否可以拦截 - if (!this.taskInterceptor.canIntercept(currentTask)) { - throw new Error(`任务 ${currentTask.id} 当前状态不可拦截`); - } - - // 执行拦截 - const interceptedTask = await this.taskInterceptor.intercept(currentTask); - if (!interceptedTask) { - throw new Error(`任务 ${currentTask.id} 拦截失败`); - } - - // 创建重定向结果 - let newTask: AgentTask | undefined; - if (newDirection.newTask) { - newTask = newDirection.newTask; - this.emit('taskRedirected', { - originalTaskId: currentTask.id, - newTaskId: newTask.id, - reason: newDirection.reason, - }); - } - - // 从任务管理中移除原任务 - this.activeTasks.delete(currentTask.id); - - // 添加到历史记录 - this.addToTaskHistory(currentTask); - - return { - success: true, - originalTask: interceptedTask, - newTask, - redirectTime: Date.now() - startTime, - }; - } catch (error) { - this.error(`任务重定向失败: ${currentTask.id}`, error as Error); - return { - success: false, - originalTask: currentTask, - redirectTime: Date.now() - startTime, - error: (error as Error).message, - }; - } - } - - /** - * 向任务队列发送Steering指令 - */ - public sendSteeringCommand( - direction: SteeringDirection, - priority: 'high' | 'medium' | 'low' = 'medium' - ): void { - const messageEvent: MessageEvent = { - id: this.generateMessageId(), - type: 'steering', - payload: direction, - timestamp: Date.now(), - priority, - }; - - this.messageQueue.push(messageEvent); - } - - /** - * 注册活动任务 - */ - public registerActiveTask(task: AgentTask): void { - this.activeTasks.set(task.id, { - ...task, - metadata: { - ...task.metadata, - startTime: Date.now(), - }, - }); - - this.emit('taskRegistered', { taskId: task.id, task }); - } - - /** - * 注销活动任务 - */ - public unregisterActiveTask(taskId: string): void { - const task = this.activeTasks.get(taskId); - if (task) { - this.activeTasks.delete(taskId); - this.addToTaskHistory(task); - this.emit('taskUnregistered', { taskId, task }); - } - } - - /** - * 获取活动任务列表 - */ - public getActiveTasks(): AgentTask[] { - return Array.from(this.activeTasks.values()); - } - - /** - * 获取历史任务 - */ - public getTaskHistory(limit = 50): AgentTask[] { - return this.taskHistory.slice(-limit); - } - - /** - * 添加到历史记录 - */ - private addToTaskHistory(task: AgentTask): void { - this.taskHistory.push({ ...task }); - - // 保持历史记录大小在限制范围内 - if (this.taskHistory.length > this.maxHistorySize) { - this.taskHistory.shift(); - } - } - - /** - * 获取控制器状态 - */ - public getStatus(): { - initialized: boolean; - steeringLoopActive: boolean; - messageQueueSize: number; - activeTaskCount: number; - taskHistoryCount: number; - } { - return { - initialized: this.isInitialized, - steeringLoopActive: this.isSteeringLoopActive, - messageQueueSize: this.messageQueue.size(), - activeTaskCount: this.activeTasks.size, - taskHistoryCount: this.taskHistory.length, - }; - } - - /** - * 销毁控制器 - */ - public async destroy(): Promise { - this.log('销毁增强型Steering控制器...'); - - try { - this.isInitialized = false; - this.isSteeringLoopActive = false; - - if (this.steeringLoopInterval) { - clearInterval(this.steeringLoopInterval); - this.steeringLoopInterval = undefined; - } - - this.activeTasks.clear(); - this.taskHistory.length = 0; - this.messageQueue.clear(); - - this.removeAllListeners(); - this.log('增强型Steering控制器已销毁'); - } catch (error) { - this.error('销毁失败', error as Error); - throw error; - } - } - - private handleStatusMessage(message: MessageEvent): void { - this.emit('statusUpdate', message.payload); - } - - private handleErrorMessage(message: MessageEvent): void { - this.emit('errorOccurred', message.payload); - } - - private generateMessageId(): string { - return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - } - - private log(message: string, data?: unknown): void { - console.log(`[EnhancedSteeringController] ${message}`, data || ''); - } - - private error(message: string, error?: Error): void { - console.error(`[EnhancedSteeringController] ${message}`, error || ''); - } - - private async redirectTask( - targetTaskId: string, - direction: SteeringDirection - ): Promise { - const task = this.activeTasks.get(targetTaskId); - if (!task) { - this.error(`Target task not found: ${targetTaskId}`); - return; - } - - this.log(`Redirecting task ${targetTaskId}`, direction); - - // Remove from active tasks - this.activeTasks.delete(targetTaskId); - - // Add to history - this.addToTaskHistory(task); - - // Emit redirect event - this.emit('taskRedirected', { - originalTaskId: targetTaskId, - reason: direction.reason || 'Task redirected', - }); - } -} diff --git a/src/agent/ParallelSubAgentExecutor.ts b/src/agent/ParallelSubAgentExecutor.ts deleted file mode 100644 index 1afd3a19..00000000 --- a/src/agent/ParallelSubAgentExecutor.ts +++ /dev/null @@ -1,684 +0,0 @@ -/** - * SubAgent并行执行系统 - * 实现Claude Code风格的SubAgent隔离并行执行 - */ - -import { EventEmitter } from 'events'; -import { ConfigManager } from '../config/ConfigManager.js'; -import { Agent } from './Agent.js'; -import type { AgentTask, SubAgentResult } from './types.js'; - -export interface IsolatedEnvironment { - id: string; - task: AgentTask; - context: SubAgentContext; - status: 'pending' | 'running' | 'completed' | 'failed'; - result?: SubAgentResult; - error?: Error; - startTime?: number; - endTime?: number; - pid?: number; // 进程/PID信息 -} - -export interface SubAgentContext { - workspace: string; - data: Record; - sharedMemory: SharedMemoryAccess; - isolationLevel: 'full' | 'partial' | 'minimal'; - constraints: IsolationConstraints; - tokens?: { input: number; output: number }; -} - -export interface SharedMemoryAccess { - read: string[]; - write: string[]; - lock: boolean; -} - -export interface IsolationConstraints { - maxExecutionTime: number; - maxMemoryUsage: number; - maxCpuUsage: number; - allowedTools: string[]; - forbiddenOperations: string[]; - sandbox?: boolean; -} - -export interface ParallelExecutionOptions { - maxParallelism?: number; - isolationLevel?: 'full' | 'partial' | 'minimal'; - enableSharedMemory?: boolean; - resourceLimits?: ResourceLimits; - fallbackStrategy?: 'fail_fast' | 'best_effort' | 'ignore'; - monitoring?: boolean; - healthCheck?: boolean; -} - -export interface ResourceLimits { - maxMemory?: number; // MB - maxCpu?: number; // CPU核心数 - maxTime?: number; // ms -} - -export interface ParallelExecutionResult { - results: SubAgentResult[]; - failed: FailedSubAgentResult[]; - succeeded: SubAgentResult[]; - executionTime: number; - parallelEfficiency: number; - resourceUsage: ResourceUsageStats; -} - -export interface FailedSubAgentResult { - agentName: string; - taskId: string; - error: Error; - isolationId: string; - executionTime: number; -} - -export interface ResourceUsageStats { - totalMemory: number; - peakMemory: number; - totalCpu: number; - executionTime: number; - isolationInstances: number; -} - -/** - * SubAgent并行执行器 - 实现并行隔离执行 - */ -export class ParallelSubAgentExecutor extends EventEmitter { - private readonly maxParallelism: number; - private readonly resourceLimits: ResourceLimits; - private readonly isInitialized: boolean = false; - private activeEnvironments = new Map(); - private sharedMemory = new Map(); - private resourceMonitor: ResourceMonitor; - - constructor(options: ParallelExecutionOptions = {}) { - super(); - - this.maxParallelism = options.maxParallelism ?? 5; - this.resourceLimits = options.resourceLimits || { - maxMemory: 500, // 500MB - maxCpu: 2, // 2核心 - maxTime: 120000, // 2分钟 - }; - - this.resourceMonitor = new ResourceMonitor(this.resourceLimits); - - if (options.monitoring) { - this.startResourceMonitoring(); - } - } - - /** - * 初始化执行器 - */ - public async initialize(): Promise { - if (this.isInitialized) { - return; - } - - try { - this.log('初始化SubAgent并行执行器...'); - - // 建立资源监控器 - await this.resourceMonitor.initialize(); - - this.log('SubAgent并行执行器初始化完成'); - this.emit('initialized'); - } catch (error) { - this.error('执行器初始化失败', error as Error); - throw error; - } - } - - /** - * 并行执行多个任务 - */ - public async executeInParallel( - tasks: AgentTask[], - options?: ParallelExecutionOptions - ): Promise { - const startTime = Date.now(); - - try { - this.log(`开始并行执行 ${tasks.length} 个任务`); - - // 创建隔离环境 - const isolatedEnvironments = await this.createIsolatedEnvironments( - tasks, - options - ); - - // 验证资源约束 - await this.validateResourceConstraints(isolatedEnvironments); - - // 执行并行任务 - const executionPromises = isolatedEnvironments.map((env) => - this.executeInIsolation(env, options) - ); - - // 使用 allSettled 获取所有执行结果 - const settledResults = await Promise.allSettled(executionPromises); - - // 处理执行结果 - const result = this.processParallelResults(settledResults, startTime); - - this.log(`并行执行完成`, { - totalTime: result.executionTime, - succeeded: result.succeeded.length, - failed: result.failed.length, - efficiency: result.parallelEfficiency, - }); - - return result; - } catch (error) { - this.error('并行执行失败', error as Error); - throw error; - } - } - - /** - * 创建隔离执行环境 - */ - private async createIsolatedEnvironments( - tasks: AgentTask[], - options?: ParallelExecutionOptions - ): Promise { - const environments: IsolatedEnvironment[] = []; - const _isolationLevel = options?.isolationLevel || 'full'; - - for (const task of tasks) { - const environmentId = this.generateIsolationId(task); - - // 创建子代理上下文 - const context = await this.createSubAgentContext(task, options); - - const environment: IsolatedEnvironment = { - id: environmentId, - task, - context, - status: 'pending', - }; - - environments.push(environment); - this.activeEnvironments.set(environmentId, environment); - } - - return environments; - } - - /** - * 创建SubAgent上下文 - */ - private async createSubAgentContext( - task: AgentTask, - options?: ParallelExecutionOptions - ): Promise { - const isolationLevel = options?.isolationLevel || 'full'; - const enableSharedMemory = options?.enableSharedMemory ?? false; - - // 创建独立工作空间 - const workspace = await this.createIsolatedWorkspace(task, isolationLevel); - - // 设置共享内存访问 - const sharedMemoryAccess: SharedMemoryAccess = { - read: enableSharedMemory ? ['common_data'] : [], - write: enableSharedMemory ? ['result_data'] : [], - lock: false, - }; - - // 设置隔离约束 - const constraints: IsolationConstraints = { - maxExecutionTime: this.resourceLimits.maxTime || 120000, - maxMemoryUsage: this.resourceLimits.maxMemory || 500, - maxCpuUsage: this.resourceLimits.maxCpu || 2, - allowedTools: this.getTaskAllowedTools(task), - forbiddenOperations: ['system_call', 'network_raw'], // 禁止潜在危险操作 - sandbox: isolationLevel === 'full', - }; - - return { - workspace, - data: task.context || {}, - sharedMemory: sharedMemoryAccess, - isolationLevel, - constraints, - }; - } - - /** - * 在隔离环境中执行单个任务 - */ - private async executeInIsolation( - environment: IsolatedEnvironment, - options?: ParallelExecutionOptions - ): Promise { - const startTime = Date.now(); - const { task, context } = environment; - - try { - environment.status = 'running'; - environment.startTime = startTime; - - this.log(`开始执行隔离任务: ${task.id}`); - this.emit('taskStarted', { taskId: task.id, isolationId: environment.id }); - - // 创建代理实例(轻量级) - const subAgent = await this.createSubAgent(task, context); - - // 设置执行超时 - const timeout = setTimeout(() => { - throw new Error(`任务执行超时: ${task.id}`); - }, context.constraints.maxExecutionTime); - - try { - // 执行任务 - const response = await this.executeTaskInIsolation(subAgent, task, context); - - clearTimeout(timeout); - - const executionTime = Date.now() - startTime; - environment.status = 'completed'; - environment.endTime = Date.now(); - - const result: SubAgentResult = { - agentName: (task.metadata?.agentName as string) || 'sub-agent', - taskType: task.type, - result: response, - executionTime, - }; - - environment.result = result; - - this.log(`任务执行成功: ${task.id}`, { executionTime }); - this.emit('taskCompleted', { taskId: task.id, result, executionTime }); - - return result; - } finally { - // 清理资源 - await this.cleanupSubAgent(subAgent); - } - } catch (error) { - environment.status = 'failed'; - environment.endTime = Date.now(); - environment.error = error as Error; - - this.error(`任务执行失败: ${task.id}`, error as Error); - this.emit('taskFailed', { taskId: task.id, error }); - - throw error; - } finally { - this.activeEnvironments.delete(environment.id); - } - } - - /** - * 创建隔离工作空间 - */ - private async createIsolatedWorkspace( - task: AgentTask, - isolationLevel: string - ): Promise { - const taskId = task.id || 'unknown'; - const workspaceId = `subagent_${taskId}_${Date.now()}`; - - switch (isolationLevel) { - case 'full': - // 完全隔离:独立的文件系统空间 - return `/tmp/subagent/${workspaceId}`; - case 'partial': - // 部分隔离:共享部分资源但有权限限制 - return `/workspace/subagent/${workspaceId}`; - case 'minimal': - // 最小隔离:主要靠权限控制 - return `/shared/subagent/${workspaceId}`; - default: - throw new Error(`不支持的隔离级别: ${isolationLevel}`); - } - } - - /** - * 创建SubAgent实例 - */ - private async createSubAgent( - task: AgentTask, - context: SubAgentContext - ): Promise { - // 获取 BladeConfig 并创建 Agent - const configManager = ConfigManager.getInstance(); - await configManager.initialize(); - const config = configManager.getConfig(); - - // 创建Agent实例(使用共享配置,空运行时选项) - return new Agent(config, {}); - } - - /** - * 在隔离环境中执行任务 - */ - private async executeTaskInIsolation( - subAgent: Agent, - task: AgentTask, - context: SubAgentContext - ): Promise { - // 设置资源限制 - const resourceController = new ResourceController(context.constraints); - - try { - // 挂载共享内存 - if (context.sharedMemory.read.length > 0) { - await this.mountSharedMemory(subAgent, context.sharedMemory.read, 'read'); - } - - // 执行任务 - const result = await resourceController.executeWithLimits(async () => { - // 模拟执行(实际应该调用Agent的执行逻辑) - return await subAgent.executeTask(task); - }); - - // 写回结果到共享内存 - if (context.sharedMemory.write.length > 0) { - await this.writeSharedMemory(result, context.sharedMemory.write); - } - - return result; - } finally { - // 清理资源限制 - await resourceController.cleanup(); - } - } - - /** - * 验证资源约束 - */ - private async validateResourceConstraints( - environments: IsolatedEnvironment[] - ): Promise { - const totalResources = this.calculateTotalResourceRequirements(environments); - const availableResources = await this.resourceMonitor.getAvailableResources(); - - // 检查内存限制 - if (totalResources.totalMemory > availableResources.availableMemory) { - throw new Error( - `内存不足: 需要 ${totalResources.totalMemory}MB, 可用 ${availableResources.availableMemory}MB` - ); - } - - // 检查CPU限制 - if (totalResources.totalCpu > availableResources.availableCpu) { - throw new Error( - `CPU核心数不足: 需要 ${totalResources.totalCpu}, 可用 ${availableResources.availableCpu}` - ); - } - - // 检查并行数量限制 - if (environments.length > this.maxParallelism) { - throw new Error( - `超过最大并行数: ${environments.length} > ${this.maxParallelism}` - ); - } - - this.log('资源约束验证通过', totalResources); - } - - /** - * 计算总资源需求 - */ - private calculateTotalResourceRequirements(environments: IsolatedEnvironment[]): { - totalMemory: number; - totalCpu: number; - totalTime: number; - } { - let totalMemory = 0; - let totalCpu = 0; - let totalTime = 0; - - for (const env of environments) { - totalMemory += env.context.constraints.maxMemoryUsage; - totalCpu += env.context.constraints.maxCpuUsage; - totalTime += env.context.constraints.maxExecutionTime; - } - - return { totalMemory, totalCpu, totalTime }; - } - - /** - * 处理并行执行结果 - */ - private processParallelResults( - settledResults: PromiseSettledResult[], - startTime: number - ): ParallelExecutionResult { - const results: SubAgentResult[] = []; - const failed: FailedSubAgentResult[] = []; - const succeeded: SubAgentResult[] = []; - let _totalExecutionTime = 0; - - for (let i = 0; i < settledResults.length; i++) { - const result = settledResults[i]; - - if (result.status === 'fulfilled') { - const successResult = result.value; - results.push(successResult); - succeeded.push(successResult); - _totalExecutionTime += successResult.executionTime; - } else { - const reason = result.reason as Error; - const task = this.getTaskFromSettledIndex(i); - const isolationId = task?.id || 'unknown'; - - const failedResult: FailedSubAgentResult = { - agentName: (task?.metadata?.agentName as string) || 'unknown', - taskId: task?.id || 'unknown', - error: reason, - isolationId, - executionTime: Date.now() - startTime, - }; - - failed.push(failedResult); - } - } - - const overallExecutionTime = Date.now() - startTime; - const parallelEfficiency = succeeded.length / settledResults.length; - - const resourceUsage = this.resourceMonitor.getResourceUsageStats(); - - return { - results, - failed, - succeeded, - executionTime: overallExecutionTime, - parallelEfficiency, - resourceUsage, - }; - } - - /** - * 获取任务(根据索引) - */ - private getTaskFromSettledIndex(index: number): AgentTask | undefined { - const environments = Array.from(this.activeEnvironments.values()); - return environments[index]?.task; - } - - /** - * 启动资源监控 - */ - private startResourceMonitoring(): void { - this.resourceMonitor.start(); - this.log('资源监控已启动'); - } - - /** - * 停止资源监控 - */ - private stopResourceMonitoring(): void { - this.resourceMonitor.stop(); - this.log('资源监控已停止'); - } - - /** - * 挂载共享内存 - */ - private async mountSharedMemory( - agent: Agent, - keys: string[], - access: 'read' | 'write' - ): Promise { - for (const key of keys) { - if (this.sharedMemory.has(key)) { - // 将共享内存数据注入代理上下文 - const _data = this.sharedMemory.get(key); - // TODO: 实现具体的内存挂载逻辑 - } - } - } - - /** - * 写入共享内存 - */ - private async writeSharedMemory(data: unknown, keys: string[]): Promise { - for (const key of keys) { - this.sharedMemory.set(key, data); - } - } - - /** - * 获取任务允许的工具列表 - */ - private getTaskAllowedTools(task: AgentTask): string[] { - // 根据任务类型和安全策略返回允许的工具 - return ['*']; // 默认允许所有工具 - } - - /** - * 清理SubAgent资源 - */ - private async cleanupSubAgent(subAgent: Agent): Promise { - // 清理代理相关资源 - if (subAgent && typeof subAgent.destroy === 'function') { - await subAgent.destroy(); - } - } - - /** - * 生成隔离ID - */ - private generateIsolationId(task: AgentTask): string { - return `iso_${task.id}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - } - - private log(message: string, data?: unknown): void { - console.log(`[ParallelSubAgentExecutor] ${message}`, data || ''); - } - - private error(message: string, error?: Error): void { - console.error(`[ParallelSubAgentExecutor] ${message}`, error || ''); - } -} - -/** - * 资源监控器 - */ -class ResourceMonitor { - private isActive = false; - private monitoringInterval?: NodeJS.Timeout; - private readonly resourceLimits: ResourceLimits; - private resourceUsage: ResourceUsageStats = { - totalMemory: 0, - peakMemory: 0, - totalCpu: 0, - executionTime: 0, - isolationInstances: 0, - }; - - constructor(resourceLimits: ResourceLimits) { - this.resourceLimits = resourceLimits; - } - - async initialize(): Promise { - // 初始化资源监控 - } - - start(): void { - if (this.isActive) return; - - this.isActive = true; - this.monitoringInterval = setInterval(() => { - this.updateResourceStats(); - }, 1000); // 每秒更新一次 - } - - stop(): void { - if (!this.isActive) return; - - this.isActive = false; - if (this.monitoringInterval) { - clearInterval(this.monitoringInterval); - } - } - - async getAvailableResources(): Promise<{ - availableMemory: number; - availableCpu: number; - }> { - // 模拟获取可用资源(实际需要读取系统信息) - return { - availableMemory: - (this.resourceLimits.maxMemory || 2000) - this.resourceUsage.totalMemory, - availableCpu: (this.resourceLimits.maxCpu || 4) - this.resourceUsage.totalCpu, - }; - } - - getResourceUsageStats(): ResourceUsageStats { - return { ...this.resourceUsage }; - } - - updateIsolationInstance(change: number): void { - this.resourceUsage.isolationInstances += change; - if (change > 0) { - this.resourceUsage.totalMemory += 100; // 每个实例约100MB - this.resourceUsage.totalCpu += 0.5; // 每个实例约0.5个CPU - } else { - this.resourceUsage.totalMemory = Math.max( - 0, - this.resourceUsage.totalMemory - 100 - ); - this.resourceUsage.totalCpu = Math.max(0, this.resourceUsage.totalCpu - 0.5); - } - - this.resourceUsage.peakMemory = Math.max( - this.resourceUsage.peakMemory, - this.resourceUsage.totalMemory - ); - } - - private updateResourceStats(): void { - this.resourceUsage.executionTime += 1; // 每秒加1秒 - // 这里可以添加更复杂的资源监控逻辑 - } -} - -/** - * 资源控制器 - */ -class ResourceController { - private readonly constraints: IsolationConstraints; - - constructor(constraints: IsolationConstraints) { - this.constraints = constraints; - } - - async executeWithLimits(operation: () => Promise): Promise { - return await operation(); - } - - async cleanup(): Promise { - // 清理资源限制 - } -} diff --git a/src/agent/subagents/SubagentExecutor.ts b/src/agent/subagents/SubagentExecutor.ts new file mode 100644 index 00000000..79ef1d3b --- /dev/null +++ b/src/agent/subagents/SubagentExecutor.ts @@ -0,0 +1,92 @@ +import type { Message } from '../../services/ChatServiceInterface.js'; +import { Agent } from '../Agent.js'; +import type { SubagentConfig, SubagentContext, SubagentResult } from './types.js'; + +/** + * Subagent 执行器 + * + * 职责: + * - 创建子 Agent 实例 + * - 配置工具白名单 + * - 执行任务并返回结果 + */ +export class SubagentExecutor { + constructor(private config: SubagentConfig) {} + + /** + * 执行 subagent 任务 + */ + async execute(context: SubagentContext): Promise { + const startTime = Date.now(); + + try { + // 1. 构建系统提示 + const systemPrompt = this.buildSystemPrompt(context); + + // 2. 创建子 Agent(使用 systemPrompt 和 toolWhitelist) + const agent = await Agent.create({ + systemPrompt, + toolWhitelist: this.config.tools, // 应用工具白名单 + }); + + // 3. 构建初始消息 + const _messages: Message[] = [ + { + role: 'user', + content: context.prompt, + }, + ]; + + // 4. 执行对话循环(让 Agent 自主完成任务) + let finalMessage = ''; + let toolCallCount = 0; + let tokensUsed = 0; + + // 使用 runAgenticLoop 让 subagent 自主执行 + const loopResult = await agent.runAgenticLoop(context.prompt, { + messages: [], + userId: 'subagent', + sessionId: context.parentSessionId || `subagent_${Date.now()}`, + workspaceRoot: process.cwd(), + }); + + if (loopResult.success) { + finalMessage = loopResult.finalMessage || ''; + toolCallCount = loopResult.metadata?.toolCallsCount || 0; + tokensUsed = loopResult.metadata?.tokensUsed || 0; + } else { + throw new Error(loopResult.error?.message || 'Subagent execution failed'); + } + + // 5. 返回结果 + const duration = Date.now() - startTime; + + return { + success: true, + message: finalMessage, + stats: { + tokens: tokensUsed, + toolCalls: toolCallCount, + duration, + }, + }; + } catch (error) { + const duration = Date.now() - startTime; + return { + success: false, + message: '', + error: error instanceof Error ? error.message : String(error), + stats: { + duration, + }, + }; + } + } + + /** + * 构建系统提示 + */ + private buildSystemPrompt(_context: SubagentContext): string { + return this.config.systemPrompt || ''; + } +} diff --git a/src/agent/subagents/SubagentRegistry.ts b/src/agent/subagents/SubagentRegistry.ts new file mode 100644 index 00000000..0b91613f --- /dev/null +++ b/src/agent/subagents/SubagentRegistry.ts @@ -0,0 +1,162 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import yaml from 'yaml'; +import type { SubagentConfig, SubagentFrontmatter } from './types.js'; + +/** + * Subagent 注册表 + * + * 职责: + * - 注册和发现 subagents + * - 解析 Markdown + YAML frontmatter 配置 + * - 生成 LLM 可读的描述 + */ +export class SubagentRegistry { + private subagents = new Map(); + + /** + * 注册一个 subagent + * @param config - 子代理配置 + */ + register(config: SubagentConfig): void { + if (this.subagents.has(config.name)) { + throw new Error(`Subagent '${config.name}' already registered`); + } + this.subagents.set(config.name, config); + } + + /** + * 获取指定 subagent + */ + getSubagent(name: string): SubagentConfig | undefined { + return this.subagents.get(name); + } + + /** + * 获取所有 subagent 名称 + */ + getAllNames(): string[] { + return Array.from(this.subagents.keys()); + } + + /** + * 获取所有 subagent 配置 + */ + getAllSubagents(): SubagentConfig[] { + return Array.from(this.subagents.values()); + } + + /** + * 生成 LLM 可读的 subagent 描述(用于系统提示) + */ + getDescriptionsForPrompt(): string { + const subagents = this.getAllSubagents(); + if (subagents.length === 0) { + return 'No subagents available.'; + } + + const descriptions = subagents.map((config) => { + let desc = `- ${config.name}: ${config.description}`; + + // 添加工具列表 + if (config.tools && config.tools.length > 0) { + desc += ` (Tools: ${config.tools.join(', ')})`; + } + + return desc; + }); + + return `Available subagent types and the tools they have access to:\n${descriptions.join('\n')}`; + } + + /** + * 从目录加载所有 subagent 配置文件 + * @param dirPath - 配置文件目录 + */ + loadFromDirectory(dirPath: string): void { + if (!fs.existsSync(dirPath)) { + return; + } + + const files = fs.readdirSync(dirPath); + for (const file of files) { + if (!file.endsWith('.md')) continue; + + const filePath = path.join(dirPath, file); + try { + const config = this.parseConfigFile(filePath); + this.register(config); + } catch (error) { + console.warn(`Failed to load subagent config from ${filePath}:`, error); + } + } + } + + /** + * 解析 Markdown + YAML frontmatter 配置文件 + */ + private parseConfigFile(filePath: string): SubagentConfig { + const content = fs.readFileSync(filePath, 'utf-8'); + + // 解析 YAML frontmatter + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!frontmatterMatch) { + throw new Error(`No YAML frontmatter found in ${filePath}`); + } + + const [, frontmatterYaml, markdownContent] = frontmatterMatch; + const frontmatter = yaml.parse(frontmatterYaml) as SubagentFrontmatter; + + // 验证必需字段 + if (!frontmatter.name || !frontmatter.description) { + throw new Error(`Missing required fields (name, description) in ${filePath}`); + } + + // 使用 Markdown 内容作为系统提示 + const systemPrompt = markdownContent.trim(); + + return { + name: frontmatter.name, + description: frontmatter.description, + systemPrompt, + tools: frontmatter.tools, + configPath: filePath, + }; + } + + /** + * 从标准位置加载所有 subagent 配置 + * + * 按优先级加载: + * 1. 用户级配置(~/.blade/agents/) + * 2. 项目级配置(.blade/agents/) + * + * @returns 加载的 subagent 数量 + */ + loadFromStandardLocations(): number { + const os = require('node:os'); + const path = require('node:path'); + + // 1. 加载用户级配置 + const userAgentsDir = path.join(os.homedir(), '.blade', 'agents'); + this.loadFromDirectory(userAgentsDir); + + // 2. 加载项目级配置 + const projectAgentsDir = path.join(process.cwd(), '.blade', 'agents'); + this.loadFromDirectory(projectAgentsDir); + + return this.getAllNames().length; + } + + /** + * 清空所有注册的 subagents(用于测试) + */ + clear(): void { + this.subagents.clear(); + } +} + +/** + * 全局单例 + */ +export const subagentRegistry = new SubagentRegistry(); diff --git a/src/agent/subagents/types.ts b/src/agent/subagents/types.ts new file mode 100644 index 00000000..a55b1bcd --- /dev/null +++ b/src/agent/subagents/types.ts @@ -0,0 +1,89 @@ +/** + * Subagent 系统类型定义 + */ + +/** + * Subagent 背景颜色 + */ +export type SubagentColor = + | 'red' + | 'blue' + | 'green' + | 'yellow' + | 'purple' + | 'orange' + | 'pink' + | 'cyan'; + +/** + * Subagent 配置 + */ +export interface SubagentConfig { + /** Subagent 唯一标识符 */ + name: string; + + /** 描述(给 LLM 看的能力说明) */ + description: string; + + /** 系统提示模板(可选,支持变量替换) */ + systemPrompt?: string; + + /** 允许的工具列表(空数组 = 所有工具) */ + tools?: string[]; + + /** UI 背景颜色(可选,用于视觉区分) */ + color?: SubagentColor; + + /** 配置文件路径(用于调试) */ + configPath?: string; +} + +/** + * Subagent 执行上下文 + */ +export interface SubagentContext { + /** 任务提示 */ + prompt: string; + + /** 父 Agent 的会话 ID(可选,用于追溯) */ + parentSessionId?: string; + + /** 父 Agent 的消息 ID(可选) */ + parentMessageId?: string; +} + +/** + * Subagent 执行结果 + */ +export interface SubagentResult { + /** 执行是否成功 */ + success: boolean; + + /** 结果消息 */ + message: string; + + /** 错误信息(如果失败) */ + error?: string; + + /** 执行统计 */ + stats?: { + /** Token 使用量 */ + tokens?: number; + + /** 工具调用次数 */ + toolCalls?: number; + + /** 执行时长(毫秒) */ + duration?: number; + }; +} + +/** + * Subagent Frontmatter(YAML 配置) + */ +export interface SubagentFrontmatter { + name: string; + description: string; + tools?: string[]; + color?: SubagentColor; +} diff --git a/src/agent/types.ts b/src/agent/types.ts index cfc065ac..024d06f5 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -33,16 +33,17 @@ export interface ChatContext { * Agent 的配置来自 ConfigManager.getConfig() (BladeConfig) */ export interface AgentOptions { - // 运行时参数 - systemPrompt?: string; // 完全替换系统提示 - appendSystemPrompt?: string; // 追加系统提示 - permissions?: Partial; // 运行时覆盖权限 - permissionMode?: PermissionMode; - maxTurns?: number; // 最大对话轮次 (-1=无限制, 0=禁用对话, N>0=限制轮次) - - // MCP 配置 - mcpConfig?: string[]; // CLI 参数:MCP 配置文件路径或 JSON 字符串数组 - strictMcpConfig?: boolean; // CLI 参数:严格模式,仅使用 --mcp-config 指定的配置 + // 运行时参数 + systemPrompt?: string; // 完全替换系统提示 + appendSystemPrompt?: string; // 追加系统提示 + permissions?: Partial; // 运行时覆盖权限 + permissionMode?: PermissionMode; + maxTurns?: number; // 最大对话轮次 (-1=无限制, 0=禁用对话, N>0=限制轮次) + toolWhitelist?: string[]; // 工具白名单(仅允许指定工具) + + // MCP 配置 + mcpConfig?: string[]; // CLI 参数:MCP 配置文件路径或 JSON 字符串数组 + strictMcpConfig?: boolean; // CLI 参数:严格模式,仅使用 --mcp-config 指定的配置 } export interface AgentTask { @@ -136,27 +137,28 @@ export interface LoopOptions { } export interface LoopResult { - success: boolean; - finalMessage?: string; - error?: { - type: - | 'canceled' - | 'max_turns_exceeded' - | 'api_error' - | 'loop_detected' - | 'aborted' - | 'chat_disabled'; - message: string; - details?: any; - }; - metadata?: { - turnsCount: number; - toolCallsCount: number; - duration: number; - configuredMaxTurns?: number; - actualMaxTurns?: number; - hitSafetyLimit?: boolean; - shouldExitLoop?: boolean; // ExitPlanMode 设置此标记以退出循环 - targetMode?: string; // Plan 模式批准后的目标权限模式(default/auto_edit) - }; + success: boolean; + finalMessage?: string; + error?: { + type: + | 'canceled' + | 'max_turns_exceeded' + | 'api_error' + | 'loop_detected' + | 'aborted' + | 'chat_disabled'; + message: string; + details?: any; + }; + metadata?: { + turnsCount: number; + toolCallsCount: number; + duration: number; + tokensUsed?: number; // Token 使用量 + configuredMaxTurns?: number; + actualMaxTurns?: number; + hitSafetyLimit?: boolean; + shouldExitLoop?: boolean; // ExitPlanMode 设置此标记以退出循环 + targetMode?: string; // Plan 模式批准后的目标权限模式(default/auto_edit) + }; } diff --git a/src/slash-commands/agents.ts b/src/slash-commands/agents.ts new file mode 100644 index 00000000..0adfdc1a --- /dev/null +++ b/src/slash-commands/agents.ts @@ -0,0 +1,160 @@ +/** + * /agents slash command - 管理 subagent 配置 + */ + +import os from 'node:os'; +import path from 'node:path'; +import { subagentRegistry } from '../agent/subagents/SubagentRegistry.js'; +import type { SlashCommand, SlashCommandContext, SlashCommandResult } from './types.js'; + +export const agentsCommand: SlashCommand = { + name: 'agents', + description: 'Manage agent configurations', + fullDescription: + 'Create, edit, or delete custom subagents. Subagents are specialized agents that Claude can delegate tasks to.', + usage: '/agents [list|create|help]', + category: 'System', + examples: ['/agents', '/agents list', '/agents help'], + + async handler(args: string[], context: SlashCommandContext): Promise { + const subcommand = args[0]; + + // 无参数 - 显示 agents 管理对话框 + if (!subcommand) { + return { + success: true, + message: 'show_agents_manager', + data: { action: 'show_agents_manager' }, + }; + } + + // list 子命令 - 显示文本列表 + if (subcommand === 'list') { + const { addAssistantMessage } = context; + const allAgents = subagentRegistry.getAllNames() + .map((name) => subagentRegistry.getSubagent(name)) + .filter((agent): agent is NonNullable => agent !== undefined); + + if (allAgents.length === 0) { + const message = + '📋 **Agents 管理**\n\n' + + '❌ 没有找到任何 agent 配置\n\n' + + '**配置文件位置:**\n' + + '- 项目级: `.blade/agents/`\n' + + '- 用户级: `~/.blade/agents/`\n\n' + + '💡 使用 `/agents` 打开管理对话框'; + + addAssistantMessage(message); + return { success: true, message: 'No agents found' }; + } + + // 按位置分组 + const projectPath = path.join(process.cwd(), '.blade', 'agents'); + const userPath = path.join(os.homedir(), '.blade', 'agents'); + + const projectAgents = allAgents.filter((a) => + a.configPath?.startsWith(projectPath), + ); + const userAgents = allAgents.filter((a) => a.configPath?.startsWith(userPath)); + + let message = `📋 **Agents 管理**\n\n找到 **${allAgents.length}** 个 agent:\n\n`; + + // 项目级 agents + if (projectAgents.length > 0) { + message += `**项目级** (.blade/agents/):\n`; + for (const agent of projectAgents) { + message += `\n• **${agent.name}**\n`; + message += ` ${agent.description}\n`; + if (agent.tools && agent.tools.length > 0) { + message += ` 工具: ${agent.tools.join(', ')}\n`; + } + if (agent.color) { + message += ` 颜色: ${agent.color}\n`; + } + } + message += '\n'; + } + + // 用户级 agents + if (userAgents.length > 0) { + message += `**用户级** (~/.blade/agents/):\n`; + for (const agent of userAgents) { + message += `\n• **${agent.name}**\n`; + message += ` ${agent.description}\n`; + if (agent.tools && agent.tools.length > 0) { + message += ` 工具: ${agent.tools.join(', ')}\n`; + } + if (agent.color) { + message += ` 颜色: ${agent.color}\n`; + } + } + message += '\n'; + } + + message += '\n💡 使用 `/agents` 打开管理对话框'; + + addAssistantMessage(message); + return { success: true, message: `Listed ${allAgents.length} agents` }; + } + + // Help 子命令 + if (subcommand === 'help') { + const { addAssistantMessage } = context; + const message = + '📋 **Agents 管理帮助**\n\n' + + '**可用子命令:**\n' + + '- `/agents list` - 列出所有已配置的 agents\n' + + '- `/agents help` - 显示此帮助信息\n\n' + + '**手动创建 Agent:**\n\n' + + '1. 在项目目录或用户目录创建 `.blade/agents/` 文件夹\n' + + '2. 创建 Markdown 文件 (如 `my-agent.md`)\n' + + '3. 使用 YAML frontmatter 定义配置:\n\n' + + '```markdown\n' + + '---\n' + + 'name: my-agent\n' + + 'description: 这个 agent 的用途和使用场景\n' + + 'tools:\n' + + ' - Glob\n' + + ' - Grep\n' + + ' - Read\n' + + 'color: blue # 可选: red/blue/green/yellow/purple/orange/pink/cyan\n' + + '---\n\n' + + '# 系统提示词\n\n' + + '你是一个专门的代理...\n' + + '```\n\n' + + '**配置优先级:**\n' + + '- 项目级 (`.blade/agents/`) - 最高优先级\n' + + '- 用户级 (`~/.blade/agents/`) - 较低优先级\n\n' + + '**可用工具:**\n' + + '- `Glob` - 文件搜索\n' + + '- `Grep` - 内容搜索\n' + + '- `Read` - 读取文件\n' + + '- `Write` - 写入文件\n' + + '- `Edit` - 编辑文件\n' + + '- `Bash` - 执行命令\n' + + '- 省略 `tools` 字段 = 继承所有工具\n\n' + + '💡 **提示:** 创建文件后,重启 Blade 使配置生效'; + + addAssistantMessage(message); + return { success: true, message: 'Help displayed' }; + } + + // create 子命令 - 显示创建对话框 + if (subcommand === 'create') { + return { + success: true, + message: 'show_agent_creation_wizard', + data: { action: 'show_agent_creation_wizard' }, + }; + } + + // 未知子命令 + const { addAssistantMessage } = context; + const message = + `❌ 未知子命令: \`${subcommand}\`\n\n` + + '使用 `/agents help` 查看可用命令'; + + addAssistantMessage(message); + return { success: false, error: `Unknown subcommand: ${subcommand}` }; + }, +}; diff --git a/src/slash-commands/builtinCommands.ts b/src/slash-commands/builtinCommands.ts index 3b4d911f..6930ffaa 100644 --- a/src/slash-commands/builtinCommands.ts +++ b/src/slash-commands/builtinCommands.ts @@ -2,6 +2,7 @@ * 内置的 slash commands */ +import { agentsCommand } from './agents.js'; import compactCommand from './compact.js'; import mcpCommand from './mcp.js'; import permissionsCommand from './permissions.js'; @@ -23,6 +24,7 @@ const helpCommand: SlashCommand = { const helpText = `🔧 **可用的 Slash Commands:** **/init** - 分析当前项目并生成 BLADE.md 配置文件 +**/agents** - 管理 subagent 配置(创建、编辑、删除) **/mcp** - 显示 MCP 服务器状态和可用工具 **/help** - 显示此帮助信息 **/clear** - 清除屏幕内容 @@ -318,4 +320,5 @@ export const builtinCommands = { resume: resumeCommand, compact: compactCommand, mcp: mcpCommand, + agents: agentsCommand, }; diff --git a/src/tools/builtin/task/cancelTask.ts b/src/tools/builtin/task/cancelTask.ts new file mode 100644 index 00000000..8345a3f0 --- /dev/null +++ b/src/tools/builtin/task/cancelTask.ts @@ -0,0 +1,141 @@ +/** + * CancelTask Tool - 取消 Subagent 任务 + */ + +import { z } from 'zod'; +import { createTool } from '../../core/createTool.js'; +import type { ExecutionContext, ToolResult } from '../../types/index.js'; +import { ToolErrorType, ToolKind } from '../../types/index.js'; +import { getTaskManager } from './task.js'; + +/** + * CancelTask 工具 - 取消任务 + */ +export const cancelTaskTool = createTool({ + name: 'CancelTask', + displayName: '取消任务', + kind: ToolKind.Execute, + isReadOnly: false, + + schema: z.object({ + task_id: z.string().describe('要取消的任务 ID'), + }), + + description: { + short: '取消正在运行或等待中的 subagent 任务', + long: ` +取消一个 subagent 任务的执行。 + +**可取消的任务:** +- pending: 等待中的任务 +- running: 正在运行的任务 + +**不可取消的任务:** +- completed: 已完成的任务 +- failed: 已失败的任务 +- cancelled: 已取消的任务 + +**注意:** +- 取消操作是异步的,任务可能不会立即停止 +- 已经执行的操作无法回滚 +- 取消后任务状态会变为 cancelled + `.trim(), + usageNotes: [ + '只能取消 pending 或 running 状态的任务', + '取消操作不会回滚已执行的操作', + '使用 TaskStatus 确认任务已取消', + ], + examples: [ + { + description: '取消任务', + params: { + task_id: 'abc123', + }, + }, + ], + }, + + async execute(params, context: ExecutionContext): Promise { + const { task_id } = params; + + try { + const taskManager = getTaskManager(); + const task = taskManager.getTask(task_id); + + if (!task) { + return { + success: false, + llmContent: `未找到任务 ${task_id}`, + displayContent: `❌ 未找到任务 ${task_id}`, + error: { + type: ToolErrorType.VALIDATION_ERROR, + message: `Task not found: ${task_id}`, + }, + }; + } + + // 检查任务状态 + if (task.status !== 'pending' && task.status !== 'running') { + return { + success: false, + llmContent: `任务 ${task_id} 无法取消,当前状态: ${task.status}`, + displayContent: `⚠️ 无法取消任务\n\n任务状态: ${task.status}\n只能取消 pending 或 running 状态的任务`, + error: { + type: ToolErrorType.VALIDATION_ERROR, + message: `Cannot cancel task in ${task.status} state`, + }, + }; + } + + // 取消任务 + const cancelled = taskManager.cancelTask(task_id); + + if (cancelled) { + return { + success: true, + llmContent: { + task_id, + status: 'cancelled', + message: '任务已取消', + }, + displayContent: + `✅ 任务已取消\n\n` + + `任务 ID: ${task_id}\n` + + `Subagent: ${task.agentName}\n` + + `描述: ${task.params.description || '无'}`, + metadata: { + task_id, + previous_status: task.status, + }, + }; + } else { + return { + success: false, + llmContent: `取消任务 ${task_id} 失败`, + displayContent: `❌ 取消任务失败\n\n任务 ID: ${task_id}`, + error: { + type: ToolErrorType.EXECUTION_ERROR, + message: 'Failed to cancel task', + }, + }; + } + } catch (error: any) { + return { + success: false, + llmContent: `取消任务失败: ${error.message}`, + displayContent: `❌ 取消任务失败\n\n${error.message}`, + error: { + type: ToolErrorType.EXECUTION_ERROR, + message: error.message, + }, + }; + } + }, + + version: '1.0.0', + category: '任务工具', + tags: ['task', 'cancel', 'control'], + + extractSignatureContent: (params) => params.task_id, + abstractPermissionRule: () => '', +}); diff --git a/src/tools/builtin/task/index.ts b/src/tools/builtin/task/index.ts index 5b9892da..4ffb5dd7 100644 --- a/src/tools/builtin/task/index.ts +++ b/src/tools/builtin/task/index.ts @@ -1,4 +1,3 @@ -// 新的基于 Zod 的工具定义 +// Subagent Task 工具导出 -export type { TaskResult } from './task.js'; -export { setTaskToolAgentFactory, TaskManager, TaskStatus, taskTool } from './task.js'; +export { taskTool } from './task.js'; diff --git a/src/tools/builtin/task/task.ts b/src/tools/builtin/task/task.ts index 0b1904cf..038c8c19 100644 --- a/src/tools/builtin/task/task.ts +++ b/src/tools/builtin/task/task.ts @@ -1,578 +1,214 @@ -import { randomUUID } from 'crypto'; +/** + * Task Tool - Subagent 调度工具 + * + * 1. Markdown + YAML frontmatter 配置 subagent + * 2. 模型决策 - 让模型自己决定用哪个 subagent_type + * 3. subagent_type 参数必需 - 明确指定要使用的 subagent + * 4. 工具隔离 - 每个 subagent 配置自己的工具白名单 + */ + import { z } from 'zod'; -import type { Agent } from '../../../agent/Agent.js'; -import type { ChatContext } from '../../../agent/types.js'; +import { SubagentExecutor } from '../../../agent/subagents/SubagentExecutor.js'; +import { subagentRegistry } from '../../../agent/subagents/SubagentRegistry.js'; +import type { + SubagentContext, + SubagentResult, +} from '../../../agent/subagents/types.js'; import { createTool } from '../../core/createTool.js'; import type { ExecutionContext, ToolResult } from '../../types/index.js'; import { ToolErrorType, ToolKind } from '../../types/index.js'; /** - * 任务状态 + * 获取可用的 subagent 类型(用于 Zod 枚举) */ -export enum TaskStatus { - PENDING = 'pending', - RUNNING = 'running', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} - -/** - * 任务结果 - */ -export interface TaskResult { - task_id: string; - status: TaskStatus; - description: string; - subagent_type?: string; - created_at: string; - started_at?: string; - completed_at?: string; - duration?: number; - result?: any; - error?: string; - background: boolean; -} - -/** - * 任务管理器 - */ -export class TaskManager { - private static instance: TaskManager; - private tasks: Map = new Map(); - - static getInstance(): TaskManager { - if (!TaskManager.instance) { - TaskManager.instance = new TaskManager(); - } - return TaskManager.instance; - } - - createTask(params: { - description: string; - subagent_type?: string; - run_in_background?: boolean; - }): TaskResult { - const taskId = randomUUID(); - const task: TaskResult = { - task_id: taskId, - status: TaskStatus.PENDING, - description: params.description, - subagent_type: params.subagent_type, - created_at: new Date().toISOString(), - background: params.run_in_background || false, - }; - - this.tasks.set(taskId, task); - return task; - } - - getTask(taskId: string): TaskResult | undefined { - return this.tasks.get(taskId); - } - - getAllTasks(): TaskResult[] { - return Array.from(this.tasks.values()).sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ); +function getAvailableSubagentTypes(): [string, ...string[]] { + const types = subagentRegistry.getAllNames(); + if (types.length === 0) { + return ['Explore']; // 默认值,避免 Zod 空数组报错 } - - updateTaskStatus( - taskId: string, - status: TaskStatus, - data?: Partial - ): void { - const task = this.tasks.get(taskId); - if (task) { - task.status = status; - - if (status === TaskStatus.RUNNING && !task.started_at) { - task.started_at = new Date().toISOString(); - } - - if ( - (status === TaskStatus.COMPLETED || status === TaskStatus.FAILED) && - !task.completed_at - ) { - task.completed_at = new Date().toISOString(); - if (task.started_at) { - task.duration = - new Date(task.completed_at).getTime() - new Date(task.started_at).getTime(); - } - } - - if (data) { - Object.assign(task, data); - } - } - } - - cancelTask(taskId: string): boolean { - const task = this.tasks.get(taskId); - if (task && task.status === TaskStatus.PENDING) { - this.updateTaskStatus(taskId, TaskStatus.CANCELLED); - return true; - } - return false; - } - - cleanupCompletedTasks(olderThanHours: number = 24): number { - const cutoffTime = new Date(Date.now() - olderThanHours * 60 * 60 * 1000); - let cleaned = 0; - - for (const [taskId, task] of this.tasks.entries()) { - if (task.completed_at && new Date(task.completed_at) < cutoffTime) { - if (task.status === TaskStatus.COMPLETED || task.status === TaskStatus.FAILED) { - this.tasks.delete(taskId); - cleaned++; - } - } - } - - return cleaned; - } -} - -// Agent 工厂函数 -let agentFactory: (() => Promise) | undefined; - -/** - * 设置 Agent 工厂函数 - */ -export function setTaskToolAgentFactory(factory: () => Promise): void { - agentFactory = factory; + return types as [string, ...string[]]; } /** - * TaskTool - Agent 任务调度工具 - * 使用新的 Zod 验证设计 + * TaskTool - Subagent 调度器 + * + * 核心设计: + * - subagent_type 参数(必需)- 明确指定使用哪个 subagent + * - 模型从 subagent 描述中选择合适的类型 + * - 每个 subagent 有独立的系统提示和工具配置 */ export const taskTool = createTool({ name: 'Task', - displayName: 'Agent任务调度', + displayName: 'Subagent调度', kind: ToolKind.Execute, - isReadOnly: true, // 🆕 显式标记为只读(启动子 Agent 不修改系统状态) + isReadOnly: true, // Zod Schema 定义 schema: z.object({ - description: z.string().min(1).describe('任务描述'), - subagent_type: z.string().optional().describe('指定子代理类型(可选)'), - prompt: z.string().optional().describe('任务提示词(可选)'), - context: z.record(z.any()).optional().describe('任务上下文数据(可选)'), - timeout: z - .number() - .int() - .min(5000) - .max(1800000) - .default(300000) - .describe('任务超时时间(毫秒,默认5分钟)'), - run_in_background: z.boolean().default(false).describe('是否在后台执行任务'), + subagent_type: z + .enum(getAvailableSubagentTypes()) + .describe('要使用的 subagent 类型(如 "Explore", "Plan")'), + description: z.string().min(3).max(100).describe('任务简短描述(3-5个词)'), + prompt: z.string().min(10).describe('详细的任务指令'), }), // 工具描述 description: { - short: '启动独立的子Agent自主执行复杂的多步骤任务', + short: + 'Launch a specialized agent to handle complex, multi-step tasks autonomously', long: ` -启动专门的子Agent来自主处理复杂任务。子Agent是独立的执行进程,拥有自己的工具访问权限和执行上下文。 - -**适用场景:** -- 需要多轮对话和工具调用才能完成的复杂任务 -- 需要大量代码搜索、探索和分析的任务 -- 需要独立执行上下文的后台任务 -- 将大任务委托给专门的子Agent处理 - -**⚠️ 重要提醒:这不是TODO清单管理工具!** -- 如需可视化跟踪任务进度清单 → 使用 TodoWrite 工具 -- 如需委托子Agent独立执行工作 → 使用 Task 工具 - -**何时不使用此工具:** -- 不要用于管理TODO任务清单(使用 TodoWrite) -- 不要用于简单的文件读取(使用 Read) -- 不要用于单个文件的代码搜索(使用 Grep) -- 不要用于已知路径的文件查找(使用 Glob) -- 不要用于简单的单步操作 +Launch a specialized agent to handle complex, multi-step tasks autonomously. + +The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it. + +${subagentRegistry.getDescriptionsForPrompt()} + +**How to use the Task tool:** +- Set subagent_type to ANY agent name from the list above (e.g., 'Explore', 'Plan', 'code-reviewer', etc.) +- Each agent has a specific purpose described in its description - choose the one that best matches the task +- The agent descriptions tell you when to use each agent (look for "Use this when...") + +**When NOT to use the Task tool:** +- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly +- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly +- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly +- Other tasks that are not related to the agent descriptions above + +**Usage notes:** +- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses +- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. +- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. +- The agent's outputs should generally be trusted +- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent +- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. +- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Task tool use content blocks. `.trim(), usageNotes: [ - '⚠️ 此工具用于启动子Agent,不是TODO清单管理!管理TODO请使用TodoWrite工具', - 'description 参数是必需的,应简短描述任务(3-5个词)', - 'prompt 参数应包含完整详细的任务指令和期望输出格式', - '可通过 subagent_type 指定特定类型的子 Agent', - '每个子Agent都是独立进程,消耗独立的资源和token', - '子Agent无法访问父Agent的对话历史,需要在prompt中提供完整上下文', - 'context 用于传递结构化的任务上下文数据', - 'timeout 默认 5 分钟,最长 30 分钟', - 'run_in_background=true 时任务在后台执行,立即返回task_id', - '后台任务需要使用 task_status 工具查看进度和结果', + 'subagent_type is required - choose from available agent types', + 'description should be 3-5 words (e.g., "Explore error handling")', + 'prompt should contain a highly detailed task description and specify exactly what information to return', + 'Launch multiple agents concurrently when possible for better performance', ], examples: [ { - description: '启动子Agent分析项目依赖', + description: 'Explore codebase for API endpoints', params: { - description: '分析项目依赖', + subagent_type: 'Explore', + description: 'Find API endpoints', prompt: - '分析项目中的所有依赖包,检查是否有过时或存在安全漏洞的包,生成详细报告包括:1) 过时包列表 2) 安全漏洞 3) 建议的更新方案', - }, - }, - { - description: '指定子代理类型执行优化', - params: { - description: '优化数据库查询', - subagent_type: 'database-optimizer', - prompt: '分析所有数据库查询语句,找出性能瓶颈,并提供优化建议', + 'Search the codebase for all API endpoint definitions. Look for route handlers, REST endpoints, and GraphQL resolvers. Return a structured list with file paths, endpoint URLs, HTTP methods, and descriptions.', }, }, { - description: '后台执行长时间测试任务', + description: 'Plan authentication feature', params: { - description: '运行完整测试套件', - prompt: '运行项目中的所有单元测试和集成测试,收集测试覆盖率,生成详细报告', - run_in_background: true, - timeout: 600000, - }, - }, - { - description: '带上下文的数据处理任务', - params: { - description: '处理用户数据', - prompt: '根据context中的user_id和action,执行相应的数据导出操作', - context: { - user_id: '12345', - action: 'export', - }, + subagent_type: 'Plan', + description: 'Plan user auth', + prompt: + 'Create a detailed implementation plan for adding user authentication to this project. Analyze the existing architecture, then provide step-by-step instructions including: 1) Database schema changes 2) API routes to create 3) Frontend components needed 4) Security considerations 5) Testing strategy. Be specific about file names and code locations.', }, }, ], - important: [ - '⚠️ 这不是TODO清单工具!管理任务清单请使用 TodoWrite', - '任务创建需要用户确认(消耗额外资源)', - '子 Agent 会消耗独立的系统资源和API token', - '子Agent是无状态的,无法访问父Agent的对话历史', - '后台任务需要手动使用 task_status 工具查看状态', - '任务超时会自动中止,建议合理设置timeout', - 'prompt 应该详细完整,包含所有必要的上下文信息', - ], }, // 执行函数 async execute(params, context: ExecutionContext): Promise { - const { - description, - prompt, - context: taskContext, - timeout = 300000, // 5分钟默认超时 - run_in_background = false, - } = params; + const { subagent_type, description, prompt } = params; const { updateOutput } = context; - const signal = context.signal ?? new AbortController().signal; try { - const taskManager = TaskManager.getInstance(); + // 1. 获取 subagent 配置 + const subagentConfig = subagentRegistry.getSubagent(subagent_type); + if (!subagentConfig) { + return { + success: false, + llmContent: `Unknown subagent type: ${subagent_type}`, + displayContent: `❌ 未知的 subagent 类型: ${subagent_type}\n\n可用类型: ${subagentRegistry.getAllNames().join(', ')}`, + error: { + type: ToolErrorType.EXECUTION_ERROR, + message: `Unknown subagent type: ${subagent_type}`, + }, + }; + } + + updateOutput?.(`🚀 启动 ${subagent_type} subagent: ${description}`); - updateOutput?.(`创建任务: ${description}`); + // 2. 创建执行器 + const executor = new SubagentExecutor(subagentConfig); - // 创建任务 - const task = taskManager.createTask(params); + // 3. 构建执行上下文 + const subagentContext: SubagentContext = { + prompt, + parentSessionId: context.sessionId, + }; - if (run_in_background) { - // 后台任务:立即返回任务ID - scheduleBackgroundTask(task, { - prompt, - context: taskContext, - timeout, - signal, - executionContext: context, // ✅ 传递 ExecutionContext - }); + updateOutput?.(`⚙️ 执行任务中...`); - const metadata = { - task_id: task.task_id, - background: true, - created_at: task.created_at, - }; + // 4. 执行 subagent + const startTime = Date.now(); + const result: SubagentResult = await executor.execute(subagentContext); + const duration = Date.now() - startTime; - const displayMessage = - `✅ 任务已创建并在后台执行\n` + - `任务ID: ${task.task_id}\n` + - `描述: ${description}\n` + - `使用 task_status 工具查看进度`; + // 5. 返回结果 + if (result.success) { + const outputPreview = + result.message.length > 1000 + ? result.message.slice(0, 1000) + '\n...(截断)' + : result.message; return { success: true, - llmContent: { - task_id: task.task_id, - status: task.status, - background: true, - description: task.description, + llmContent: result.message, + displayContent: + `✅ Subagent 任务完成\n\n` + + `类型: ${subagent_type}\n` + + `任务: ${description}\n` + + `耗时: ${duration}ms\n` + + `工具调用: ${result.stats?.toolCalls || 0} 次\n` + + `Token: ${result.stats?.tokens || 0}\n\n` + + `结果:\n${outputPreview}`, + metadata: { + subagent_type, + description, + duration, + stats: result.stats, }, - displayContent: displayMessage, - metadata, }; } else { - // 前台任务:等待完成 - return await executeTaskSync(task, { - prompt, - context: taskContext, - timeout, - signal, - updateOutput, - executionContext: context, // ✅ 传递 ExecutionContext - }); - } - } catch (error: any) { - if (error.name === 'AbortError') { return { success: false, - llmContent: '任务执行被中止', - displayContent: '⚠️ 任务执行被用户中止', + llmContent: `Subagent 执行失败: ${result.error}`, + displayContent: + `⚠️ Subagent 任务失败\n\n` + + `类型: ${subagent_type}\n` + + `任务: ${description}\n` + + `耗时: ${duration}ms\n` + + `错误: ${result.error}`, error: { type: ToolErrorType.EXECUTION_ERROR, - message: '操作被中止', + message: result.error || '未知错误', }, }; } - + } catch (error) { + const err = error as Error; return { success: false, - llmContent: `任务创建失败: ${error.message}`, - displayContent: `❌ 任务创建失败: ${error.message}`, + llmContent: `Subagent 执行异常: ${err.message}`, + displayContent: `❌ Subagent 执行异常\n\n${err.message}\n\n${err.stack || ''}`, error: { type: ToolErrorType.EXECUTION_ERROR, - message: error.message, + message: err.message, details: error, }, }; } }, - version: '2.0.0', - category: '任务工具', - tags: ['task', 'agent', 'schedule', 'workflow'], - - /** - * 提取签名内容:返回任务描述和提示组合 - */ - extractSignatureContent: (params) => `${params.description} | ${params.prompt}`, + version: '4.0.0', + category: 'Subagent', + tags: ['task', 'subagent', 'delegation', 'explore', 'plan'], - /** - * 抽象权限规则:Task 工具禁用自动生成规则 - * 返回空字符串表示不自动添加权限规则 - */ + extractSignatureContent: (params) => `${params.subagent_type}:${params.description}`, abstractPermissionRule: () => '', }); - -/** - * 后台任务调度 - */ -function scheduleBackgroundTask( - task: TaskResult, - options: { - prompt?: string; - context?: Record; - timeout: number; - signal: AbortSignal; - executionContext?: ExecutionContext; // ✅ 添加执行上下文参数 - } -): void { - const taskManager = TaskManager.getInstance(); - - // 异步执行任务 - setTimeout(async () => { - try { - taskManager.updateTaskStatus(task.task_id, TaskStatus.RUNNING); - - // 模拟任务执行(实际应该调用相应的Agent) - const result = await simulateTaskExecution(task, options); - - taskManager.updateTaskStatus(task.task_id, TaskStatus.COMPLETED, { - result, - }); - } catch (error: any) { - taskManager.updateTaskStatus(task.task_id, TaskStatus.FAILED, { - error: error.message, - }); - } - }, 0); -} - -/** - * 同步执行任务 - */ -async function executeTaskSync( - task: TaskResult, - options: { - prompt?: string; - context?: Record; - timeout: number; - signal: AbortSignal; - updateOutput?: (output: string) => void; - executionContext?: ExecutionContext; // ✅ 添加执行上下文参数 - } -): Promise { - const taskManager = TaskManager.getInstance(); - - try { - options.updateOutput?.(`开始执行任务: ${task.description}`); - taskManager.updateTaskStatus(task.task_id, TaskStatus.RUNNING); - - const result = await simulateTaskExecution(task, options); - - taskManager.updateTaskStatus(task.task_id, TaskStatus.COMPLETED, { - result, - }); - - const completedTask = taskManager.getTask(task.task_id)!; - - const metadata = { - task_id: task.task_id, - duration: completedTask.duration, - completed_at: completedTask.completed_at, - }; - - const displayMessage = formatDisplayMessage(completedTask); - - return { - success: true, - llmContent: completedTask, - displayContent: displayMessage, - metadata, - }; - } catch (error: any) { - taskManager.updateTaskStatus(task.task_id, TaskStatus.FAILED, { - error: error.message, - }); - - const failedTask = taskManager.getTask(task.task_id)!; - - return { - success: false, - llmContent: `任务执行失败: ${error.message}`, - displayContent: `❌ 任务执行失败: ${error.message}`, - error: { - type: ToolErrorType.EXECUTION_ERROR, - message: error.message, - details: { - task_id: task.task_id, - error: error.message, - failed_at: failedTask.completed_at, - }, - }, - }; - } -} - -/** - * TODO 模拟任务执行 - */ -async function simulateTaskExecution( - task: TaskResult, - options: { - prompt?: string; - context?: Record; - timeout: number; - signal: AbortSignal; - executionContext?: ExecutionContext; // 添加执行上下文参数 - } -): Promise { - // 尝试使用真实的子 Agent - if (agentFactory) { - console.log('🚀 使用真实子 Agent 执行任务...'); - try { - // 创建子 Agent - const subAgent = await agentFactory(); - - // 构建完整的 ChatContext,传递 confirmationHandler - const subContext: ChatContext = { - messages: [], // 子任务从空消息列表开始 - userId: (options.context?.userId as string) || 'subagent', - sessionId: (options.context?.sessionId as string) || `subagent_${Date.now()}`, - workspaceRoot: (options.context?.workspaceRoot as string) || process.cwd(), - signal: options.signal, - confirmationHandler: options.executionContext?.confirmationHandler, - }; - - // 调用 runAgenticLoop - const result = await subAgent.runAgenticLoop( - options.prompt || task.description, - subContext, - { - maxTurns: 10, // 子任务限制为 10 轮 - signal: options.signal, - } - ); - - if (result.success) { - return { - task_description: task.description, - subagent_type: task.subagent_type || 'general', - execution_result: result.finalMessage, - metadata: result.metadata, - timestamp: new Date().toISOString(), - }; - } else { - throw new Error(result.error?.message || '子任务执行失败'); - } - } catch (error) { - console.error('子 Agent 执行失败:', error); - throw error; - } - } - - // 降级:使用模拟逻辑 - console.log('⚠️ 未配置 agentFactory,使用模拟逻辑'); - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - reject(new Error('任务执行超时')); - }, options.timeout); - - const abortHandler = () => { - clearTimeout(timeoutId); - reject(new Error('任务被用户中止')); - }; - - options.signal.addEventListener('abort', abortHandler); - - // 模拟任务处理时间 - setTimeout( - () => { - clearTimeout(timeoutId); - options.signal.removeEventListener('abort', abortHandler); - - resolve({ - task_description: task.description, - subagent_type: task.subagent_type || 'general', - execution_result: `任务 "${task.description}" 已成功完成(模拟)`, - context: options.context, - timestamp: new Date().toISOString(), - }); - }, - Math.random() * 2000 + 1000 - ); // 1-3秒随机延迟 - }); -} - -/** - * 格式化显示消息 - */ -function formatDisplayMessage(task: TaskResult): string { - let message = `✅ 任务执行完成: ${task.description}`; - message += `\n任务ID: ${task.task_id}`; - message += `\n状态: ${task.status}`; - - if (task.duration) { - message += `\n执行时间: ${task.duration}ms`; - } - - if (task.result) { - const resultPreview = - typeof task.result === 'object' - ? JSON.stringify(task.result, null, 2) - : String(task.result); - - if (resultPreview.length > 500) { - message += `\n执行结果:\n${resultPreview.substring(0, 500)}...(已截断)`; - } else { - message += `\n执行结果:\n${resultPreview}`; - } - } - - return message; -} diff --git a/src/tools/builtin/task/taskList.ts b/src/tools/builtin/task/taskList.ts new file mode 100644 index 00000000..c6fdf5f0 --- /dev/null +++ b/src/tools/builtin/task/taskList.ts @@ -0,0 +1,168 @@ +/** + * TaskList Tool - 列出 Subagent 任务 + */ + +import { z } from 'zod'; +import { createTool } from '../../core/createTool.js'; +import type { ExecutionContext, ToolResult } from '../../types/index.js'; +import { ToolKind } from '../../types/index.js'; +import { getTaskManager } from './task.js'; + +/** + * TaskList 工具 - 列出任务 + */ +export const taskListTool = createTool({ + name: 'TaskList', + displayName: '列出任务', + kind: ToolKind.Read, + isReadOnly: true, + + schema: z.object({ + status: z + .enum(['pending', 'running', 'completed', 'failed', 'cancelled']) + .optional() + .describe('按状态过滤'), + agent_name: z.string().optional().describe('按 subagent 名称过滤'), + limit: z.number().int().positive().default(10).describe('返回的最大任务数'), + }), + + description: { + short: '列出 subagent 任务', + long: ` +列出所有或特定条件的 subagent 任务。 + +**过滤选项:** +- status: 按状态过滤(pending, running, completed, failed, cancelled) +- agent_name: 按 subagent 类型过滤 +- limit: 限制返回数量(默认 10) + +**返回信息:** +- 任务 ID +- 状态 +- Subagent 类型 +- 创建时间 +- 简要描述 + +**适用场景:** +- 查看所有运行中的任务 +- 查看最近完成的任务 +- 查看特定 subagent 的任务历史 + `.trim(), + usageNotes: [ + '默认返回最近的 10 个任务', + '使用 status 参数查看特定状态的任务', + '使用 agent_name 参数查看特定 subagent 的任务', + ], + examples: [ + { + description: '列出所有运行中的任务', + params: { + status: 'running', + }, + }, + { + description: '列出最近 5 个已完成的任务', + params: { + status: 'completed', + limit: 5, + }, + }, + { + description: '列出 file-search 的任务', + params: { + agent_name: 'file-search', + }, + }, + ], + }, + + async execute(params, context: ExecutionContext): Promise { + const { status, agent_name, limit = 10 } = params; + + try { + const taskManager = getTaskManager(); + const tasks = taskManager.listTasks({ + status, + agentName: agent_name, + limit, + }); + + if (tasks.length === 0) { + return { + success: true, + llmContent: { tasks: [], count: 0 }, + displayContent: '📋 没有找到任务', + }; + } + + // 格式化输出 + let displayContent = `📋 找到 ${tasks.length} 个任务\n\n`; + + for (const task of tasks) { + const statusEmoji = { + pending: '⏳', + running: '🔄', + completed: '✅', + failed: '❌', + cancelled: '🚫', + }[task.status]; + + displayContent += `${statusEmoji} ${task.id.slice(0, 8)}... - ${task.agentName}\n`; + displayContent += ` 状态: ${task.status}\n`; + displayContent += ` 创建: ${new Date(task.createdAt).toLocaleString()}\n`; + + if (task.params.description) { + displayContent += ` 描述: ${task.params.description}\n`; + } + + if (task.result) { + displayContent += ` 回合: ${task.result.turns}, 耗时: ${task.result.duration}ms\n`; + } + + displayContent += '\n'; + } + + // 统计信息 + const stats = taskManager.getStats(); + displayContent += `\n统计: 总计 ${stats.total} 个任务, 运行中 ${stats.running} 个`; + + return { + success: true, + llmContent: { + tasks: tasks.map((t) => ({ + task_id: t.id, + status: t.status, + agent_name: t.agentName, + description: t.params.description, + created_at: t.createdAt, + completed_at: t.completedAt, + })), + count: tasks.length, + stats, + }, + displayContent, + metadata: { + count: tasks.length, + stats, + }, + }; + } catch (error: any) { + return { + success: false, + llmContent: `列出任务失败: ${error.message}`, + displayContent: `❌ 列出任务失败\n\n${error.message}`, + error: { + type: 'execution_error', + message: error.message, + }, + }; + } + }, + + version: '1.0.0', + category: '任务工具', + tags: ['task', 'list', 'query'], + + extractSignatureContent: () => 'list_tasks', + abstractPermissionRule: () => '', +}); diff --git a/src/tools/builtin/task/taskStatus.ts b/src/tools/builtin/task/taskStatus.ts new file mode 100644 index 00000000..7749d067 --- /dev/null +++ b/src/tools/builtin/task/taskStatus.ts @@ -0,0 +1,174 @@ +/** + * TaskStatus Tool - 查询 Subagent 任务状态 + */ + +import { z } from 'zod'; +import { createTool } from '../../core/createTool.js'; +import type { ExecutionContext, ToolResult } from '../../types/index.js'; +import { ToolErrorType, ToolKind } from '../../types/index.js'; +import { getTaskManager } from './task.js'; + +/** + * TaskStatus 工具 - 查询任务状态 + */ +export const taskStatusTool = createTool({ + name: 'TaskStatus', + displayName: '查询任务状态', + kind: ToolKind.Read, + isReadOnly: true, + + schema: z.object({ + task_id: z.string().describe('任务 ID'), + }), + + description: { + short: '查询 subagent 任务的执行状态和结果', + long: ` +查询后台或已完成的 subagent 任务的状态。 + +**返回信息:** +- 任务状态(pending, running, completed, failed, cancelled) +- 执行进度(已完成的回合数) +- 执行结果(如果已完成) +- Token 使用情况 +- 错误信息(如果失败) + +**适用场景:** +- 查看后台任务的进度 +- 获取已完成任务的结果 +- 检查任务是否失败 + `.trim(), + usageNotes: [ + '使用 Task 工具返回的 task_id 查询状态', + '后台任务需要定期查询以获取最新状态', + '已完成的任务会返回完整的执行结果', + ], + examples: [ + { + description: '查询任务状态', + params: { + task_id: 'abc123', + }, + }, + ], + }, + + async execute(params, context: ExecutionContext): Promise { + const { task_id } = params; + + try { + const taskManager = getTaskManager(); + const task = taskManager.getTask(task_id); + + if (!task) { + return { + success: false, + llmContent: `未找到任务 ${task_id}`, + displayContent: `❌ 未找到任务 ${task_id}`, + error: { + type: ToolErrorType.VALIDATION_ERROR, + message: `Task not found: ${task_id}`, + }, + }; + } + + // 格式化状态 + const statusEmoji = { + pending: '⏳', + running: '🔄', + completed: '✅', + failed: '❌', + cancelled: '🚫', + }[task.status]; + + const statusText = { + pending: '等待中', + running: '运行中', + completed: '已完成', + failed: '失败', + cancelled: '已取消', + }[task.status]; + + let displayContent = `${statusEmoji} 任务状态: ${statusText}\n\n`; + displayContent += `任务 ID: ${task_id}\n`; + displayContent += `Subagent: ${task.agentName}\n`; + displayContent += `创建时间: ${new Date(task.createdAt).toLocaleString()}\n`; + + if (task.startedAt) { + displayContent += `开始时间: ${new Date(task.startedAt).toLocaleString()}\n`; + } + + if (task.completedAt) { + displayContent += `完成时间: ${new Date(task.completedAt).toLocaleString()}\n`; + const duration = task.completedAt - task.createdAt; + displayContent += `总耗时: ${duration}ms\n`; + } + + // 如果有结果 + if (task.result) { + displayContent += `\n回合数: ${task.result.turns}\n`; + displayContent += `执行时长: ${task.result.duration}ms\n`; + + if (task.result.tokenUsage) { + displayContent += `Token 使用: ${task.result.tokenUsage.total}\n`; + } + + displayContent += `终止原因: ${task.result.terminateReason}\n`; + + if (task.result.output) { + displayContent += `\n结果:\n`; + const outputText = + typeof task.result.output === 'string' + ? task.result.output + : JSON.stringify(task.result.output, null, 2); + + displayContent += + outputText.length > 500 + ? outputText.slice(0, 500) + '...(截断)' + : outputText; + } + } + + // 如果有错误 + if (task.error) { + displayContent += `\n错误: ${task.error}\n`; + } + + return { + success: true, + llmContent: { + task_id, + status: task.status, + agent_name: task.agentName, + result: task.result, + error: task.error, + created_at: task.createdAt, + started_at: task.startedAt, + completed_at: task.completedAt, + }, + displayContent, + metadata: { + task_id, + status: task.status, + }, + }; + } catch (error: any) { + return { + success: false, + llmContent: `查询任务状态失败: ${error.message}`, + displayContent: `❌ 查询失败\n\n${error.message}`, + error: { + type: ToolErrorType.EXECUTION_ERROR, + message: error.message, + }, + }; + } + }, + + version: '1.0.0', + category: '任务工具', + tags: ['task', 'status', 'query'], + + extractSignatureContent: (params) => params.task_id, + abstractPermissionRule: () => '', +}); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index c73a2fba..d068824d 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,5 +1,6 @@ import { useMemoizedFn } from 'ahooks'; import React, { useEffect, useState } from 'react'; +import { subagentRegistry } from '../agent/subagents/SubagentRegistry.js'; import type { GlobalOptions } from '../cli/types.js'; import { ConfigManager, mergeRuntimeConfig } from '../config/ConfigManager.js'; import { DEFAULT_CONFIG } from '../config/defaults.js'; @@ -57,6 +58,19 @@ export const AppWrapper: React.FC = (props) => { } } + // 5. 预加载 subagents 配置(确保 AgentsManager 可以立即使用) + try { + const loadedCount = subagentRegistry.loadFromStandardLocations(); + if (props.debug && loadedCount > 0) { + console.log(`✓ 已加载 ${loadedCount} 个 subagents: ${subagentRegistry.getAllNames().join(', ')}`); + } + } catch (error) { + // 静默失败,不影响应用启动 + if (props.debug) { + console.warn('⚠️ Subagents 加载失败:', formatErrorMessage(error)); + } + } + setIsInitialized(true); } catch (error) { // 静默失败,使用默认配置 diff --git a/src/ui/components/AgentCreationWizard.tsx b/src/ui/components/AgentCreationWizard.tsx new file mode 100644 index 00000000..e220d076 --- /dev/null +++ b/src/ui/components/AgentCreationWizard.tsx @@ -0,0 +1,810 @@ +/** + * AgentCreationWizard - Agent 创建向导 + * + * 交互式多步骤表单,用于创建自定义 subagent 配置 + * + * 步骤流程: + * Step 1: 输入 Agent 名称(kebab-case) + * Step 2: 输入描述信息 + * Step 3: 选择工具列表(多选) + * Step 4: 选择背景颜色 + * Step 5: 选择配置位置(项目/用户) + * Step 6: 输入系统提示词 + * Step 7: 确认并保存 + */ + +import { useMemoizedFn } from 'ahooks'; +import { MultiSelect } from '@inkjs/ui'; +import { Box, Text, useFocus, useFocusManager, useInput } from 'ink'; +import Spinner from 'ink-spinner'; +import SelectInput from 'ink-select-input'; +import TextInput from 'ink-text-input'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { useEffect, useState } from 'react'; +import { Agent } from '../../agent/Agent.js'; +import type { SubagentColor } from '../../agent/subagents/types.js'; +import { useCtrlCHandler } from '../hooks/useCtrlCHandler.js'; + +interface AgentCreationWizardProps { + /** 完成回调 */ + onComplete: () => void; + /** 取消回调 */ + onCancel: () => void; + /** 编辑模式:传入现有配置 */ + initialConfig?: AgentConfig; +} + +interface AgentConfig { + name: string; + description: string; + tools: string[]; + color?: SubagentColor; + location: 'project' | 'user'; + systemPrompt: string; +} + +type WizardStep = + | 'mode' // 选择手动 or AI 生成 + | 'aiPrompt' // AI 生成:输入描述 + | 'aiGenerating' // AI 生成中 + | 'name' + | 'description' + | 'tools' + | 'color' + | 'location' + | 'systemPrompt' + | 'confirm'; + +// 可用工具列表 +const AVAILABLE_TOOLS = [ + { label: '🔍 Glob - 文件搜索', value: 'Glob' }, + { label: '🔎 Grep - 内容搜索', value: 'Grep' }, + { label: '📖 Read - 读取文件', value: 'Read' }, + { label: '✍️ Write - 写入文件', value: 'Write' }, + { label: '✏️ Edit - 编辑文件', value: 'Edit' }, + { label: '💻 Bash - 执行命令', value: 'Bash' }, + { label: '✅ 所有工具 (不限制)', value: 'all' }, +]; + +// 可用颜色 +const AVAILABLE_COLORS: Array<{ label: string; value: SubagentColor | 'none' }> = [ + { label: '🔴 红色 (red)', value: 'red' }, + { label: '🔵 蓝色 (blue)', value: 'blue' }, + { label: '🟢 绿色 (green)', value: 'green' }, + { label: '🟡 黄色 (yellow)', value: 'yellow' }, + { label: '🟣 紫色 (purple)', value: 'purple' }, + { label: '🟠 橙色 (orange)', value: 'orange' }, + { label: '🩷 粉色 (pink)', value: 'pink' }, + { label: '🩵 青色 (cyan)', value: 'cyan' }, + { label: '⚪ 不设置颜色', value: 'none' }, +]; + +/** + * 验证 agent 名称(kebab-case) + */ +function validateAgentName(name: string): string | null { + if (!name || name.trim() === '') { + return '名称不能为空'; + } + if (!/^[a-z0-9-]+$/.test(name)) { + return '名称只能包含小写字母、数字和连字符'; + } + if (name.startsWith('-') || name.endsWith('-')) { + return '名称不能以连字符开头或结尾'; + } + return null; +} + +/** + * Agent 创建向导主组件 + */ +export function AgentCreationWizard({ + onComplete, + onCancel, + initialConfig, +}: AgentCreationWizardProps) { + const [currentStep, setCurrentStep] = useState( + initialConfig ? 'name' : 'mode' // 编辑模式跳过选择,直接进入手动配置 + ); + const [config, setConfig] = useState({ + name: initialConfig?.name || '', + description: initialConfig?.description || '', + tools: initialConfig?.tools || [], + color: initialConfig?.color, + location: initialConfig?.location || 'project', + systemPrompt: initialConfig?.systemPrompt || '', + }); + const [aiPrompt, setAiPrompt] = useState(''); + const [aiGenerating, setAiGenerating] = useState(false); + const [aiError, setAiError] = useState(''); + const [workflowType, setWorkflowType] = useState<'manual' | 'ai' | 'edit'>( + initialConfig ? 'edit' : 'manual' + ); + + const { focus } = useFocusManager(); + + // 定义不同流程的步骤序列 + const workflows = { + manual: [ + 'mode', + 'name', + 'description', + 'tools', + 'color', + 'location', + 'systemPrompt', + 'confirm', + ] as WizardStep[], + ai: ['mode', 'aiPrompt', 'aiGenerating', 'confirm'] as WizardStep[], + edit: [ + 'name', + 'description', + 'tools', + 'color', + 'location', + 'systemPrompt', + 'confirm', + ] as WizardStep[], + }; + + // 获取当前流程的步骤序列 + const currentWorkflow = workflows[workflowType]; + + // 步骤切换时更新焦点 + useEffect(() => { + if ( + currentStep === 'name' || + currentStep === 'description' || + currentStep === 'systemPrompt' || + currentStep === 'aiPrompt' + ) { + // TextInput 步骤不设置焦点(让其自然获得键盘控制) + return; + } + // SelectInput 步骤设置焦点 + focus(`step-${currentStep}`); + }, [currentStep, focus]); + + // 下一步 + const nextStep = useMemoizedFn(() => { + const currentIndex = currentWorkflow.indexOf(currentStep); + if (currentIndex < currentWorkflow.length - 1) { + setCurrentStep(currentWorkflow[currentIndex + 1]); + } + }); + + // 上一步 + const prevStep = useMemoizedFn(() => { + // 重置 AI 错误状态(如果有) + if (aiError) { + setAiError(''); + } + + // 特殊处理: 从 confirm 返回时,如果在 AI 工作流,跳过 aiGenerating 直接回到 aiPrompt + if (currentStep === 'confirm' && workflowType === 'ai') { + setCurrentStep('aiPrompt'); + return; + } + + const currentIndex = currentWorkflow.indexOf(currentStep); + if (currentIndex > 0) { + setCurrentStep(currentWorkflow[currentIndex - 1]); + } else { + // 已经是第一步,退出向导 + onCancel(); + } + }); + + // AI 生成配置 + const generateConfigWithAI = useMemoizedFn(async () => { + setAiGenerating(true); + setAiError(''); + + try { + // 创建一个新的 Agent 实例用于 AI 生成 + const agent = await Agent.create(); + + // 构建系统提示 + const systemPrompt = `你是一个 Subagent 配置生成专家。用户会描述他们想要创建的 agent,你需要根据描述生成一个完整的 agent 配置。 + +## 可用工具列表 + +- **Glob**: 文件模式匹配(例如:查找所有 .ts 文件) +- **Grep**: 代码内容搜索(例如:搜索特定函数调用) +- **Read**: 读取文件内容 +- **Write**: 写入/创建文件 +- **Edit**: 编辑文件(字符串替换) +- **Bash**: 执行命令行命令 + +## 可用颜色 + +- red, blue, green, yellow, purple, orange, pink, cyan + +## 输出格式 + +你必须以 JSON 格式返回配置,格式如下: + +\`\`\`json +{ + "name": "agent-name", + "description": "简洁的一句话描述(英文)", + "tools": ["Glob", "Grep", "Read"], + "color": "blue", + "systemPrompt": "详细的系统提示词,说明这个 agent 的职责、工作方式、输出格式等" +} +\`\`\` + +## 注意事项 + +1. **name**: 只能包含小写字母、数字和连字符(kebab-case),例如:code-reviewer +2. **description**: 一句话说明用途,建议以 "Fast agent specialized for..." 开头 +3. **tools**: 根据任务选择必要的工具,不要选择太多 +4. **color**: 选择一个合适的颜色用于 UI 区分 +5. **systemPrompt**: 详细说明 agent 的职责、使用的工具、输出格式等,使用 Markdown 格式 + +**重要**:请直接返回 JSON,不要添加任何额外的解释或文字。`; + + // 调用 LLM 生成配置 + const response = await agent.chatWithSystem(systemPrompt, aiPrompt); + + // 解析响应 + let jsonStr = response.trim(); + + // 移除 markdown 代码块标记 + const codeBlockMatch = jsonStr.match(/```(?:json)?\s*\n([\s\S]*?)\n```/); + if (codeBlockMatch) { + jsonStr = codeBlockMatch[1]; + } + + const parsed = JSON.parse(jsonStr); + + // 验证必需字段 + if (!parsed.name || !parsed.description || !parsed.systemPrompt) { + throw new Error('Missing required fields: name, description, or systemPrompt'); + } + + // 验证 name 格式(kebab-case) + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(parsed.name)) { + throw new Error('Invalid name format. Must be kebab-case (lowercase, numbers, hyphens only)'); + } + + // 将生成的配置应用到 config 状态 + setConfig({ + name: parsed.name, + description: parsed.description, + tools: Array.isArray(parsed.tools) ? parsed.tools : [], + color: parsed.color || 'blue', + location: 'project', // 默认项目级 + systemPrompt: parsed.systemPrompt, + }); + + // 跳转到确认步骤 + setCurrentStep('confirm'); + } catch (error) { + setAiError(error instanceof Error ? error.message : String(error)); + } finally { + setAiGenerating(false); + } + }); + + // 当进入 aiGenerating 步骤时触发 AI 生成 + useEffect(() => { + if (currentStep === 'aiGenerating' && !aiGenerating && !aiError) { + generateConfigWithAI(); + } + }, [currentStep, aiGenerating, aiError, generateConfigWithAI]); + + // 保存配置 + const saveConfig = useMemoizedFn(async () => { + try { + // 确定保存路径 + const baseDir = + config.location === 'project' + ? path.join(process.cwd(), '.blade', 'agents') + : path.join(os.homedir(), '.blade', 'agents'); + + // 确保目录存在 + await fs.promises.mkdir(baseDir, { recursive: true }); + + // 生成文件内容 + const frontmatter = [ + '---', + `name: ${config.name}`, + `description: ${config.description}`, + ]; + + // 添加工具列表(如果不是"所有工具") + if (config.tools.length > 0 && !config.tools.includes('all')) { + frontmatter.push('tools:'); + for (const tool of config.tools) { + frontmatter.push(` - ${tool}`); + } + } + + // 添加颜色(如果设置了) + if (config.color) { + frontmatter.push(`color: ${config.color}`); + } + + frontmatter.push('---'); + + const fileContent = [ + frontmatter.join('\n'), + '', + `# ${config.name} Subagent`, + '', + config.systemPrompt || '你是一个专门的代理,负责执行特定任务。', + '', + ].join('\n'); + + // 写入文件 + const filePath = path.join(baseDir, `${config.name}.md`); + await fs.promises.writeFile(filePath, fileContent, 'utf-8'); + + onComplete(); + } catch (error) { + // TODO: 显示错误消息 + console.error('保存配置失败:', error); + } + }); + + // 使用智能 Ctrl+C 处理(向导没有执行中状态,直接退出) + const handleCtrlC = useCtrlCHandler(false, onCancel); + + // ESC 键处理:返回上一步 + // Ctrl+C 处理:智能退出 + // 注意:tools 步骤由 ToolsSelectionStep 自己处理 ESC + useInput( + (input, key) => { + if (key.escape) { + prevStep(); // prevStep 内部会判断是否是第一步,如果是则调用 onCancel() + } else if ((key.ctrl && input === 'c') || (key.meta && input === 'c')) { + handleCtrlC(); + } + }, + { isActive: currentStep !== 'tools' } + ); + + // 步骤 0: 选择创建模式 + if (currentStep === 'mode') { + return ( + + + + 🎯 选择创建方式 + + + + 你可以手动配置每个细节,或让 AI 根据你的描述自动生成配置 + + { + if (item.value === 'ai') { + setWorkflowType('ai'); + setCurrentStep('aiPrompt'); + } else { + setWorkflowType('manual'); + setCurrentStep('name'); + } + }} + /> + + 使用方向键选择 | Enter 确认 | ESC 取消 + + + ); + } + + // 步骤 AI.1: AI 生成提示词输入 + if (currentStep === 'aiPrompt') { + return ( + + + + 🤖 AI 智能生成 + + + + + 描述你想要创建的 Agent(例如:"一个专门用于代码审查的 + agent,能够分析代码质量和潜在bug") + + + + 描述: + { + if (!value.trim()) { + return; + } + setCurrentStep('aiGenerating'); + }} + /> + + + 按 Enter 开始生成 | ESC 返回 + + + ); + } + + // 步骤 AI.2: AI 生成中 + if (currentStep === 'aiGenerating') { + if (aiError) { + return ( + + + + ❌ AI 生成失败 + + + + {aiError} + + + 按 ESC 返回修改描述 + + + ); + } + + return ( + + + + AI 正在生成配置... + + + + 根据你的描述:"{aiPrompt}" + + + + 正在调用 LLM 生成 agent 配置,请稍候... + + + + ); + } + + // 步骤 1: 名称输入 + if (currentStep === 'name') { + const isEditMode = workflowType === 'edit'; + + return ( + + + + 📝 Step 1/7: {isEditMode ? 'Agent 名称(不可修改)' : '输入 Agent 名称'} + + + + + {isEditMode + ? '编辑模式下名称不可修改(修改名称相当于创建新 Agent)' + : '名称只能包含小写字母、数字和连字符(例如:code-reviewer)'} + + + + 名称: + {isEditMode ? ( + {config.name} + ) : ( + setConfig({ ...config, name: value })} + onSubmit={(value) => { + const error = validateAgentName(value); + if (error) { + // TODO: 显示错误提示 + return; + } + nextStep(); + }} + /> + )} + + + + {isEditMode ? '按 Enter 继续 | ESC 返回' : '按 Enter 继续 | ESC 返回'} + + + {isEditMode && ( + { + // 编辑模式下的隐藏输入,仅用于捕获 Enter 键 + }} + onSubmit={nextStep} + showCursor={false} + /> + )} + + ); + } + + // 步骤 2: 描述输入 + if (currentStep === 'description') { + return ( + + + + 📝 Step 2/7: 输入描述信息 + + + + + 简短描述这个 Agent 的用途和使用场景(这将帮助主 Agent 决定何时使用它) + + + + 描述: + setConfig({ ...config, description: value })} + onSubmit={(value) => { + if (!value.trim()) { + return; + } + nextStep(); + }} + /> + + + 按 Enter 继续 | ESC 返回 + + + ); + } + + // 步骤 3: 工具选择 + if (currentStep === 'tools') { + return ( + + ); + } + + // 步骤 4: 颜色选择 + if (currentStep === 'color') { + return ( + + + + 🎨 Step 4/7: 选择背景颜色 + + + + + 为 Agent 选择一个颜色标识(用于在 UI 中区分不同的 Agents) + + + { + const color = + item.value === 'none' ? undefined : (item.value as SubagentColor); + setConfig({ ...config, color }); + nextStep(); + }} + /> + + 使用方向键选择 | Enter 确认 | ESC 返回 + + + ); + } + + // 步骤 5: 位置选择 + if (currentStep === 'location') { + return ( + + + + 📂 Step 5/7: 选择保存位置 + + + + 项目级配置仅在当前项目生效,用户级配置全局可用 + + { + setConfig({ ...config, location: item.value as 'project' | 'user' }); + nextStep(); + }} + /> + + 使用方向键选择 | Enter 确认 | ESC 返回 + + + ); + } + + // 步骤 6: 系统提示词 + if (currentStep === 'systemPrompt') { + return ( + + + + 💬 Step 6/7: 输入系统提示词 + + + + 定义 Agent 的行为和职责(可选,留空将使用默认提示) + + + 提示词: + setConfig({ ...config, systemPrompt: value })} + onSubmit={() => nextStep()} + /> + + + 按 Enter 继续 | ESC 返回 + + + ); + } + + // 步骤 7: 确认 + if (currentStep === 'confirm') { + return ( + + + + ✅ Step 7/7: 确认配置 + + + + + + + 名称:{' '} + + {config.name} + + + + 描述:{' '} + + {config.description} + + + + 工具:{' '} + + + {config.tools.includes('all') + ? '所有工具' + : config.tools.join(', ') || '所有工具'} + + + + + 颜色:{' '} + + {config.color || '默认'} + + + + 位置:{' '} + + {config.location === 'project' ? '项目级' : '用户级'} + + + + 提示词:{' '} + + {config.systemPrompt || '(使用默认)'} + + + + { + if (item.value === 'save') { + saveConfig(); + } else if (item.value === 'back') { + prevStep(); + } else { + onCancel(); + } + }} + /> + + ); + } + + return null; +} + +/** + * 工具选择步骤组件(多选) + */ +interface ToolsSelectionStepProps { + config: AgentConfig; + setConfig: (config: AgentConfig) => void; + onNext: () => void; + onPrev: () => void; +} + +function ToolsSelectionStep({ + config, + setConfig, + onNext, + onPrev, +}: ToolsSelectionStepProps) { + const { isFocused } = useFocus({ id: 'step-tools' }); + + // 处理 ESC 键返回上一步 + useInput( + (_input, key) => { + if (key.escape) { + onPrev(); + } + }, + { isActive: isFocused } + ); + + const handleSubmit = (selectedValues: string[]) => { + // 如果选择了"所有工具",只保留 'all' + if (selectedValues.includes('all')) { + setConfig({ ...config, tools: ['all'] }); + } else { + setConfig({ ...config, tools: selectedValues }); + } + + onNext(); + }; + + return ( + + + + 🔧 Step 3/7: 选择可用工具 + + + + + 方向键导航,空格切换勾选,Enter 确认进入下一步 | ESC 返回上一步 + + + + + ); +} diff --git a/src/ui/components/AgentsManager.tsx b/src/ui/components/AgentsManager.tsx new file mode 100644 index 00000000..e25288f7 --- /dev/null +++ b/src/ui/components/AgentsManager.tsx @@ -0,0 +1,362 @@ +/** + * AgentsManager - Subagent 配置管理器 + * + * 交互式 UI,用于创建、编辑、删除 subagent 配置 + */ + +import { useMemoizedFn } from 'ahooks'; +import { Box, Text, useInput } from 'ink'; +import SelectInput from 'ink-select-input'; +import fs from 'node:fs'; +import { useMemo, useState } from 'react'; +import { subagentRegistry } from '../../agent/subagents/SubagentRegistry.js'; +import type { SubagentConfig } from '../../agent/subagents/types.js'; +import { useCtrlCHandler } from '../hooks/useCtrlCHandler.js'; +import { AgentCreationWizard } from './AgentCreationWizard.js'; + +type ViewMode = + | 'menu' + | 'list' + | 'create' + | 'edit' + | 'editWizard' + | 'delete' + | 'deleteConfirm'; + +export interface AgentsManagerProps { + /** 初始视图模式 */ + initialMode?: ViewMode; + /** 完成回调 */ + onComplete?: () => void; + /** 取消回调 */ + onCancel?: () => void; +} + +interface MenuItem { + key?: string; + label: string; + value: string; +} + +/** + * Subagent 配置管理器主组件 + */ +export function AgentsManager({ + initialMode = 'menu', + onComplete, + onCancel, +}: AgentsManagerProps) { + const [mode, setMode] = useState(initialMode); + const [selectedAgent, setSelectedAgent] = useState(null); + const [refreshKey, setRefreshKey] = useState(0); // 用于触发重新加载 + + // 重新加载 registry + const reloadAgents = useMemoizedFn(() => { + subagentRegistry.clear(); + subagentRegistry.loadFromStandardLocations(); + setRefreshKey((prev) => prev + 1); // 触发重新渲染 + }); + + // 加载所有已配置的 agents (依赖 refreshKey 重新计算) + const allAgents = useMemo(() => { + return subagenctRegistry.getAllSubagents(); + }, [refreshKey]); + + // 主菜单选项 + const menuItems: MenuItem[] = [ + { key: 'list', label: '📋 查看所有 Agents', value: 'list' }, + { key: 'create', label: '➕ 创建新 Agent', value: 'create' }, + { key: 'edit', label: '✏️ 编辑 Agent', value: 'edit' }, + { key: 'delete', label: '🗑️ 删除 Agent', value: 'delete' }, + { key: 'cancel', label: '❌ 取消', value: 'cancel' }, + ]; + + // 主菜单选择处理 + const handleMenuSelect = useMemoizedFn((item: MenuItem) => { + if (item.value === 'cancel') { + onCancel?.(); + return; + } + setMode(item.value as ViewMode); + }); + + // Agent 选择处理(编辑/删除) + const handleAgentSelect = useMemoizedFn( + (item: { label: string; value: SubagentConfig }) => { + setSelectedAgent(item.value); + // 根据当前模式决定下一步 + if (mode === 'edit') { + setMode('editWizard'); + } else if (mode === 'delete') { + setMode('deleteConfirm'); + } + } + ); + + // 删除 Agent + const handleDelete = useMemoizedFn(async () => { + if (!selectedAgent?.configPath) { + return; + } + + try { + // 删除配置文件 + await fs.promises.unlink(selectedAgent.configPath); + // 重新加载 agents + reloadAgents(); + // 返回主菜单 + backToMenu(); + } catch (error) { + console.error('删除失败:', error); + } + }); + + // 返回主菜单 + const backToMenu = useMemoizedFn(() => { + setMode('menu'); + setSelectedAgent(null); + }); + + // 使用智能 Ctrl+C 处理(AgentsManager 没有执行中状态,直接退出) + const handleCtrlC = useCtrlCHandler(false, onCancel); + + // ESC 键处理:返回上一步或取消 + // Ctrl+C 处理:智能退出 + // 注意:create 和 editWizard 模式下不拦截 ESC,让向导组件自己处理 + useInput( + (input, key) => { + if (key.escape) { + if (mode === 'menu') { + // 主菜单按 ESC 退出 + onCancel?.(); + } else if (mode === 'list' || mode === 'edit' || mode === 'delete') { + // 列表/选择视图返回主菜单 + backToMenu(); + } else if (mode === 'deleteConfirm') { + // 删除确认返回上一步 + backToMenu(); + } + // create 和 editWizard 模式:不处理,让向导组件自己处理 ESC + } else if ((key.ctrl && input === 'c') || (key.meta && input === 'c')) { + handleCtrlC(); + } + }, + { isActive: mode !== 'create' && mode !== 'editWizard' } + ); + + // 主菜单视图 + if (mode === 'menu') { + return ( + + + + 📋 Agents 管理 + + + + + 使用方向键选择 | Enter 确认 | ESC 退出 + + + ); + } + + // 列表视图 + if (mode === 'list') { + if (allAgents.length === 0) { + return ( + + + + 📋 所有 Agents + + + + ❌ 没有找到任何 agent 配置 + + + + 💡 配置文件位置: .blade/agents/ 或 ~/.blade/agents/ + + + + 按 ESC 返回菜单 + + + ); + } + + return ( + + + + 📋 所有 Agents + + (找到 {allAgents.length} 个) + + + {allAgents.map((agent) => ( + + + + + • {agent.name} + + - {agent.description} + + + {agent.tools && agent.tools.length > 0 && ( + + 工具: {agent.tools.join(', ')} + + )} + {agent.configPath && ( + + + {agent.configPath} + + + )} + + ))} + + + 按 ESC 返回菜单 + + + ); + } + + // Agent 选择器视图(编辑/删除共用) + const renderAgentSelector = (title: string) => { + if (allAgents.length === 0) { + return ( + + + + {title} + + + + ❌ 没有找到任何 agent 配置 + + + 按 ESC 返回菜单 + + + ); + } + + const agentItems = allAgents.map((agent) => ({ + key: agent.name, // 显式指定 key 避免 React 警告 + label: `${agent.name} - ${agent.description}`, + value: agent, + })); + + return ( + + + + {title} + + + + + 使用方向键选择 | Enter 确认 | ESC 返回 + + + ); + }; + + // 创建视图 + if (mode === 'create') { + return ( + { + reloadAgents(); // 重新加载 agents + backToMenu(); // 返回主菜单,可以查看新创建的 agent + }} + onCancel={backToMenu} + /> + ); + } + + // 编辑视图 - 选择要编辑的 Agent + if (mode === 'edit') { + return renderAgentSelector('✏️ 编辑 Agent'); + } + + // 编辑向导 - 使用 AgentCreationWizard 编辑选中的 Agent + if (mode === 'editWizard' && selectedAgent) { + // 将 SubagentConfig 转换为 AgentConfig 格式 + const initialConfig = { + name: selectedAgent.name, + description: selectedAgent.description, + tools: selectedAgent.tools || [], + color: selectedAgent.color, + location: selectedAgent.configPath?.includes('.blade/agents') + ? ('project' as const) + : ('user' as const), + systemPrompt: selectedAgent.systemPrompt || '', + }; + + return ( + { + reloadAgents(); // 重新加载 agents + backToMenu(); // 返回主菜单 + }} + onCancel={backToMenu} + /> + ); + } + + // 删除视图 - 选择要删除的 Agent + if (mode === 'delete') { + return renderAgentSelector('🗑️ 删除 Agent'); + } + + // 删除确认 - 确认删除选中的 Agent + if (mode === 'deleteConfirm' && selectedAgent) { + return ( + + + + ⚠️ 确认删除 + + + + + 你确定要删除 Agent{' '} + + {selectedAgent.name} + {' '} + 吗? + + + + 文件路径: {selectedAgent.configPath} + + + 此操作无法撤销! + + { + if (item.value === 'confirm') { + handleDelete(); + } else { + backToMenu(); + } + }} + /> + + ); + } + + return null; +} diff --git a/src/ui/components/BladeInterface.tsx b/src/ui/components/BladeInterface.tsx index 66946600..463cbf29 100644 --- a/src/ui/components/BladeInterface.tsx +++ b/src/ui/components/BladeInterface.tsx @@ -2,7 +2,11 @@ import { useMemoizedFn } from 'ahooks'; import { Box, useApp } from 'ink'; import React, { useEffect, useRef } from 'react'; import { ConfigManager } from '../../config/ConfigManager.js'; -import { PermissionMode, type SetupConfig, type ModelConfig } from '../../config/types.js'; +import { + PermissionMode, + type ModelConfig, + type SetupConfig, +} from '../../config/types.js'; import { createLogger, LogCategory } from '../../logging/Logger.js'; import { SessionService } from '../../services/SessionService.js'; import type { ConfirmationResponse } from '../../tools/types/ExecutionTypes.js'; @@ -15,6 +19,8 @@ import { useCommandHistory } from '../hooks/useCommandHistory.js'; import { useConfirmation } from '../hooks/useConfirmation.js'; import { useInputBuffer } from '../hooks/useInputBuffer.js'; import { useMainInput } from '../hooks/useMainInput.js'; +import { AgentCreationWizard } from './AgentCreationWizard.js'; +import { AgentsManager } from './AgentsManager.js'; import { ChatStatusBar } from './ChatStatusBar.js'; import { CommandSuggestions } from './CommandSuggestions.js'; import { ConfirmationPrompt } from './ConfirmationPrompt.js'; @@ -172,19 +178,18 @@ export const BladeInterface: React.FC = ({ } }); - const { showSuggestions, suggestions, selectedSuggestionIndex } = - useMainInput( - inputBuffer, - executeCommand, - getPreviousCommand, - getNextCommand, - addToHistory, - handleAbort, - sessionState.isThinking, - handlePermissionModeToggle, - handleToggleShortcuts, - appState.activeModal === 'shortcuts' - ); + const { showSuggestions, suggestions, selectedSuggestionIndex } = useMainInput( + inputBuffer, + executeCommand, + getPreviousCommand, + getNextCommand, + addToHistory, + handleAbort, + sessionState.isThinking, + handlePermissionModeToggle, + handleToggleShortcuts, + appState.activeModal === 'shortcuts' + ); // 当有输入内容时,自动关闭快捷键帮助 useEffect(() => { @@ -348,6 +353,12 @@ export const BladeInterface: React.FC = ({ } else if (appState.activeModal === 'permissionsManager') { // 显示权限管理器时,焦点转移到管理器 setFocus(FocusId.PERMISSIONS_MANAGER); + } else if (appState.activeModal === 'agentsManager') { + // 显示 agents 管理器时,焦点转移到管理器 + setFocus(FocusId.AGENTS_MANAGER); + } else if (appState.activeModal === 'agentCreationWizard') { + // 显示 agent 创建向导时,焦点转移到向导 + setFocus(FocusId.AGENT_CREATION_WIZARD); } else if (appState.activeModal === 'shortcuts') { // 显示快捷键帮助时,焦点保持在主输入框(帮助面板可以通过 ? 或 Esc 关闭) setFocus(FocusId.MAIN_INPUT); @@ -476,6 +487,9 @@ export const BladeInterface: React.FC = ({ const inlineModelUiVisible = inlineModelSelectorVisible || Boolean(inlineModelWizardMode); + const agentsManagerVisible = appState.activeModal === 'agentsManager'; + const agentCreationWizardVisible = appState.activeModal === 'agentCreationWizard'; + const editingInitialConfig = editingModel ? { name: editingModel.name, @@ -504,7 +518,8 @@ export const BladeInterface: React.FC = ({ /> ) : null; - const isInputDisabled = sessionState.isThinking || !readyForChat || inlineModelUiVisible; + const isInputDisabled = + sessionState.isThinking || !readyForChat || inlineModelUiVisible; return ( @@ -542,7 +557,9 @@ export const BladeInterface: React.FC = ({ = ({ )} + {agentsManagerVisible && ( + + + + )} + + {agentCreationWizardVisible && ( + + + + )} + {/* 命令建议列表 - 显示在输入框下方 */} ({ + type: 'SET_ACTIVE_MODAL' as const, + payload: 'agentsManager' as ActiveModal, + }), + + showAgentCreationWizard: () => ({ + type: 'SET_ACTIVE_MODAL' as const, + payload: 'agentCreationWizard' as ActiveModal, + }), + closeModal: () => ({ type: 'CLOSE_MODAL' as const, }), diff --git a/src/ui/contexts/FocusContext.tsx b/src/ui/contexts/FocusContext.tsx index 060c09ee..39256f09 100644 --- a/src/ui/contexts/FocusContext.tsx +++ b/src/ui/contexts/FocusContext.tsx @@ -13,6 +13,8 @@ export enum FocusId { MODEL_SELECTOR = 'model-selector', MODEL_CONFIG_WIZARD = 'model-config-wizard', // 统一的模型配置向导(支持 setup 和 add 模式) PERMISSIONS_MANAGER = 'permissions-manager', + AGENTS_MANAGER = 'agents-manager', + AGENT_CREATION_WIZARD = 'agent-creation-wizard', } /** diff --git a/src/ui/hooks/useCommandHandler.ts b/src/ui/hooks/useCommandHandler.ts index c08725f9..34effa5d 100644 --- a/src/ui/hooks/useCommandHandler.ts +++ b/src/ui/hooks/useCommandHandler.ts @@ -5,9 +5,9 @@ import { ConfigManager } from '../../config/ConfigManager.js'; import { createLogger, LogCategory } from '../../logging/Logger.js'; import type { SessionMetadata } from '../../services/SessionService.js'; import { - executeSlashCommand, - isSlashCommand, - type SlashCommandContext, + executeSlashCommand, + isSlashCommand, + type SlashCommandContext, } from '../../slash-commands/index.js'; import type { TodoItem } from '../../tools/builtin/todo/types.js'; import type { ConfirmationHandler } from '../../tools/types/ExecutionTypes.js'; @@ -127,11 +127,11 @@ export const useCommandHandler = ( } = useSession(); const { dispatch: appDispatch, actions: appActions } = useAppState(); const permissionMode = usePermissionMode(); - const abortControllerRef = useRef(undefined); - const agentRef = useRef(undefined); - const abortMessageSentRef = useRef(false); + const abortControllerRef = useRef(undefined); + const agentRef = useRef(undefined); + const abortMessageSentRef = useRef(false); - // 清理函数 + // 清理函数 useEffect(() => { return () => { if (agentRef.current) { @@ -251,6 +251,18 @@ export const useCommandHandler = ( return { success: true }; } + // 检查是否需要显示 agents 管理器 + if (slashResult.message === 'show_agents_manager') { + appDispatch(appActions.showAgentsManager()); + return { success: true }; + } + + // 检查是否需要显示 agent 创建向导 + if (slashResult.message === 'show_agent_creation_wizard') { + appDispatch(appActions.showAgentCreationWizard()); + return { success: true }; + } + // 检查是否需要显示会话选择器 if (slashResult.message === 'show_session_selector') { // 传递会话数据到 AppContext