diff --git a/package-lock.json b/package-lock.json index 1a00fc1..8f99ad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "chalk": "^5.3.0", "commander": "^12.1.0", "inquirer": "^12.0.0", + "js-yaml": "^4.1.1", "ora": "^8.1.0", "tree-sitter-bash": "^0.25.1", "vscode-jsonrpc": "^8.2.1", @@ -27,6 +28,7 @@ "ai-assist": "dist/index.js" }, "devDependencies": { + "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "tsx": "^4.19.0", "typescript": "^5.6.0" @@ -912,6 +914,13 @@ "js-tiktoken": "^1.0.14" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", @@ -985,6 +994,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1540,6 +1555,18 @@ "base64-js": "^1.5.1" } }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", diff --git a/package.json b/package.json index 9ed95ef..5ede759 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "chalk": "^5.3.0", "commander": "^12.1.0", "inquirer": "^12.0.0", + "js-yaml": "^4.1.1", "ora": "^8.1.0", "tree-sitter-bash": "^0.25.1", "vscode-jsonrpc": "^8.2.1", @@ -38,6 +39,7 @@ "zod": "^4.1.13" }, "devDependencies": { + "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "tsx": "^4.19.0", "typescript": "^5.6.0" diff --git a/src/agent/config-loader.ts b/src/agent/config-loader.ts new file mode 100644 index 0000000..74755f8 --- /dev/null +++ b/src/agent/config-loader.ts @@ -0,0 +1,169 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type { AgentConfigFile } from './types.js'; + +/** + * 配置文件搜索路径 + */ +const CONFIG_PATHS = [ + '.ai-assist/agents.yaml', + '.ai-assist/agents.yml', + '.ai-assist/agents.json', + '.ai-assist.yaml', + '.ai-assist.yml', +]; + +/** + * 解析 YAML 内容 + */ +async function parseYaml(content: string): Promise { + try { + const yaml = await import('js-yaml'); + return yaml.load(content); + } catch { + console.warn('解析 YAML 配置文件失败'); + return null; + } +} + +/** + * 加载用户自定义 Agent 配置 + * @param workdir 工作目录 + * @returns 配置对象或 null + */ +export async function loadAgentConfig(workdir: string): Promise { + for (const configPath of CONFIG_PATHS) { + const fullPath = path.join(workdir, configPath); + + try { + if (!fs.existsSync(fullPath)) { + continue; + } + + const content = await fs.promises.readFile(fullPath, 'utf-8'); + + let config: unknown; + + if (configPath.endsWith('.json')) { + config = JSON.parse(content); + } else if (configPath.endsWith('.yaml') || configPath.endsWith('.yml')) { + config = await parseYaml(content); + if (!config) continue; + } else { + continue; + } + + // 验证配置格式 + if (isValidAgentConfig(config)) { + return config; + } else { + console.warn(`Agent 配置格式无效: ${fullPath}`); + } + } catch (error) { + console.warn(`加载 Agent 配置失败: ${fullPath}`, error); + } + } + + return null; +} + +/** + * 验证配置格式是否有效 + */ +function isValidAgentConfig(config: unknown): config is AgentConfigFile { + if (typeof config !== 'object' || config === null) { + return false; + } + + const obj = config as Record; + + // defaults 是可选的 + if (obj.defaults !== undefined && typeof obj.defaults !== 'object') { + return false; + } + + // agents 是可选的 + if (obj.agents !== undefined && typeof obj.agents !== 'object') { + return false; + } + + return true; +} + +/** + * 保存 Agent 配置到文件 + * @param workdir 工作目录 + * @param config 配置对象 + * @param format 文件格式 + */ +export async function saveAgentConfig( + workdir: string, + config: AgentConfigFile, + format: 'json' | 'yaml' = 'json' +): Promise { + const dir = path.join(workdir, '.ai-assist'); + + // 确保目录存在 + if (!fs.existsSync(dir)) { + await fs.promises.mkdir(dir, { recursive: true }); + } + + const filename = format === 'json' ? 'agents.json' : 'agents.yaml'; + const fullPath = path.join(dir, filename); + + let content: string; + + if (format === 'json') { + content = JSON.stringify(config, null, 2); + } else { + try { + const yaml = await import('js-yaml'); + content = yaml.dump(config, { indent: 2, lineWidth: 120 }); + } catch { + // 回退到 JSON + content = JSON.stringify(config, null, 2); + console.warn('保存 YAML 失败,已保存为 JSON 格式'); + } + } + + await fs.promises.writeFile(fullPath, content, 'utf-8'); +} + +/** + * 获取配置文件模板 + */ +export function getConfigTemplate(): AgentConfigFile { + return { + defaults: { + maxSteps: 15, + model: { + temperature: 0.7, + }, + permission: { + bash: { + rules: [ + { pattern: 'rm -rf *', action: 'deny' }, + { pattern: 'git push --force*', action: 'deny' }, + ], + }, + }, + }, + agents: { + 'custom-agent': { + description: '自定义 Agent 示例', + mode: 'subagent', + prompt: '你是一个自定义助手。', + tools: { + disabled: ['bash'], + }, + permission: { + file: { + read: 'allow', + write: 'ask', + }, + }, + maxSteps: 10, + }, + }, + }; +} diff --git a/src/agent/executor.ts b/src/agent/executor.ts new file mode 100644 index 0000000..77eaefa --- /dev/null +++ b/src/agent/executor.ts @@ -0,0 +1,300 @@ +import { createAnthropic } from '@ai-sdk/anthropic'; +import { createDeepSeek } from '@ai-sdk/deepseek'; +import { + generateText, + streamText, + stepCountIs, + type ModelMessage, + type Tool as AITool, + type LanguageModel, +} from 'ai'; +import type { Tool, ToolResult, ProviderType, AgentConfig } from '../types/index.js'; +import { buildZodSchema } from '../types/index.js'; +import { ToolRegistry } from '../tools/registry.js'; +import type { + AgentInfo, + AgentExecutionContext, + AgentExecutionResult, +} from './types.js'; +import { checkBashPermission } from './permission-merger.js'; + +// Provider 工厂函数类型 +type ProviderFactory = (apiKey: string) => (model: string) => LanguageModel; + +// Provider 注册表 +const providers: Record = { + anthropic: (apiKey) => { + const client = createAnthropic({ apiKey }); + return (model) => client(model); + }, + deepseek: (apiKey) => { + const client = createDeepSeek({ apiKey }); + return (model) => client(model); + }, +}; + +/** + * Agent 执行器 + * 根据 Agent 配置执行任务,支持工具过滤和权限控制 + */ +export class AgentExecutor { + private agentInfo: AgentInfo; + private baseConfig: AgentConfig; + private toolRegistry: ToolRegistry; + private getModel: (model: string) => LanguageModel; + + constructor( + agentInfo: AgentInfo, + baseConfig: AgentConfig, + toolRegistry: ToolRegistry + ) { + this.agentInfo = agentInfo; + this.baseConfig = baseConfig; + this.toolRegistry = toolRegistry; + + // 获取模型工厂 + const provider = agentInfo.model?.provider ?? baseConfig.provider; + const providerFactory = providers[provider]; + if (!providerFactory) { + throw new Error(`不支持的 provider: ${provider}`); + } + this.getModel = providerFactory(baseConfig.apiKey); + } + + /** + * 执行任务 + */ + async execute( + prompt: string, + context: AgentExecutionContext + ): Promise { + const { onStream, onToolCall, onToolResult } = context; + + // 获取过滤后的工具 + const tools = this.getFilteredTools(); + const vercelTools = this.buildVercelTools(tools); + + // 构建系统提示词 + const systemPrompt = this.buildSystemPrompt(); + + // 获取模型配置 + const modelName = this.agentInfo.model?.model ?? this.baseConfig.model; + const maxSteps = this.agentInfo.maxSteps ?? 10; + const maxTokens = this.agentInfo.model?.maxTokens ?? this.baseConfig.maxTokens; + + // 构建初始消息 + const messages: ModelMessage[] = [ + { + role: 'user', + content: prompt, + }, + ]; + + let fullResponse = ''; + let steps = 0; + + try { + if (onStream) { + // 流式模式 + const result = streamText({ + model: this.getModel(modelName), + system: systemPrompt, + messages, + tools: vercelTools, + maxOutputTokens: maxTokens, + stopWhen: stepCountIs(maxSteps), + onChunk: ({ chunk }) => { + if (chunk.type === 'tool-call') { + steps++; + const toolArgs = 'input' in chunk ? chunk.input : {}; + onToolCall?.(chunk.toolName, toolArgs as Record); + onStream(`\n[调用工具: ${chunk.toolName}]\n`); + } else if (chunk.type === 'tool-result') { + const output = (chunk as { output?: ToolResult }).output; + onToolResult?.( + (chunk as { toolName?: string }).toolName ?? 'unknown', + output + ); + if (output && typeof output === 'object') { + if (output.success) { + const displayOutput = + output.output.length > 500 + ? output.output.substring(0, 500) + '...(截断)' + : output.output; + onStream(`[结果: ${displayOutput}]\n`); + } else { + onStream(`[错误: ${output.error}]\n`); + } + } + } + }, + }); + + for await (const chunk of result.textStream) { + fullResponse += chunk; + onStream(chunk); + } + + await result.response; + } else { + // 非流式模式 + const result = await generateText({ + model: this.getModel(modelName), + system: systemPrompt, + messages, + tools: vercelTools, + maxOutputTokens: maxTokens, + stopWhen: stepCountIs(maxSteps), + }); + + fullResponse = result.text; + steps = result.steps.length; + } + + return { + success: true, + text: fullResponse, + steps, + sessionId: context.parentSessionId ?? 'standalone', + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + text: '', + steps, + sessionId: context.parentSessionId ?? 'standalone', + error: errorMessage, + }; + } + } + + /** + * 获取过滤后的工具列表 + */ + private getFilteredTools(): Tool[] { + const allTools = this.toolRegistry.getAllTools(); + const toolConfig = this.agentInfo.tools; + + // 如果没有工具配置,返回所有工具 + if (!toolConfig) { + return allTools; + } + + let filteredTools = allTools; + + // 如果指定了 enabled,只保留这些工具 + if (toolConfig.enabled && toolConfig.enabled.length > 0) { + const enabledSet = new Set(toolConfig.enabled); + filteredTools = filteredTools.filter((t) => enabledSet.has(t.name)); + } + + // 移除 disabled 的工具 + if (toolConfig.disabled && toolConfig.disabled.length > 0) { + const disabledSet = new Set(toolConfig.disabled); + filteredTools = filteredTools.filter((t) => !disabledSet.has(t.name)); + } + + // 如果禁止嵌套 Task,移除 task 工具 + if (toolConfig.noTask) { + filteredTools = filteredTools.filter((t) => t.name !== 'task'); + } + + return filteredTools; + } + + /** + * 构建 Vercel AI SDK 工具格式 + */ + private buildVercelTools(tools: Tool[]): Record { + const vercelTools: Record = {}; + + for (const tool of tools) { + const schema = buildZodSchema(tool.parameters); + + vercelTools[tool.name] = { + description: tool.description, + inputSchema: schema, + execute: async (params) => { + // 权限检查 + const permissionResult = await this.checkToolPermission( + tool.name, + params as Record + ); + if (!permissionResult.allowed) { + return { + success: false, + output: '', + error: `权限拒绝: ${permissionResult.reason}`, + }; + } + + return tool.execute(params as Record); + }, + } as AITool; + } + + return vercelTools; + } + + /** + * 检查工具调用权限 + */ + private async checkToolPermission( + toolName: string, + params: Record + ): Promise<{ allowed: boolean; reason?: string }> { + const permission = this.agentInfo.permission; + if (!permission) { + return { allowed: true }; + } + + // Bash 权限检查 + if (toolName === 'bash' && permission.bash) { + const command = params.command as string; + if (!command) { + return { allowed: true }; + } + + const action = checkBashPermission(command, permission.bash); + if (action === 'deny') { + return { allowed: false, reason: `命令被禁止: ${command}` }; + } + // ask 在这里视为允许(实际的 ask 逻辑在权限管理器中处理) + } + + // 文件写入权限检查 + if (['write_file', 'edit_file', 'delete_file'].includes(toolName)) { + const filePermission = permission.file; + if (filePermission) { + const operation = toolName === 'write_file' ? 'write' : + toolName === 'edit_file' ? 'edit' : 'delete'; + const action = filePermission[operation]; + if (action === 'deny') { + return { allowed: false, reason: `${operation} 操作被禁止` }; + } + } + } + + // Git 写操作权限检查 + const gitWriteTools = ['git_add', 'git_commit', 'git_push', 'git_checkout', 'git_stash']; + if (gitWriteTools.includes(toolName) && permission.git?.write === 'deny') { + return { allowed: false, reason: 'Git 写操作被禁止' }; + } + + return { allowed: true }; + } + + /** + * 构建系统提示词 + */ + private buildSystemPrompt(): string { + // 如果 Agent 有自定义 prompt,使用它 + if (this.agentInfo.prompt) { + return this.agentInfo.prompt; + } + + // 否则使用基础配置的 systemPrompt + return this.baseConfig.systemPrompt; + } +} diff --git a/src/agent/index.ts b/src/agent/index.ts new file mode 100644 index 0000000..505eb87 --- /dev/null +++ b/src/agent/index.ts @@ -0,0 +1,50 @@ +// Types +export type { + AgentMode, + PermissionAction, + PermissionRule, + AgentBashPermission, + AgentFilePermission, + AgentGitPermission, + AgentPermission, + AgentModelConfig, + AgentToolConfig, + AgentInfo, + AgentConfigFile, + AgentExecutionContext, + AgentExecutionResult, +} from './types.js'; + +// Registry +export { AgentRegistry, agentRegistry } from './registry.js'; + +// Executor +export { AgentExecutor } from './executor.js'; + +// Permission Merger +export { + SYSTEM_DEFAULT_PERMISSION, + mergePermissions, + matchRule, + checkBashPermission, + checkFilePathPermission, +} from './permission-merger.js'; + +// Config Loader +export { + loadAgentConfig, + saveAgentConfig, + getConfigTemplate, +} from './config-loader.js'; + +// Presets +export { + presetAgents, + getPresetAgentNames, + isPresetAgent, + generalAgent, + exploreAgent, + codeReviewerAgent, + buildAgent, + planAgent, +} from './presets/index.js'; diff --git a/src/agent/permission-merger.ts b/src/agent/permission-merger.ts new file mode 100644 index 0000000..73371f3 --- /dev/null +++ b/src/agent/permission-merger.ts @@ -0,0 +1,221 @@ +import type { + AgentPermission, + AgentFilePermission, + AgentBashPermission, + AgentGitPermission, + PermissionAction, + PermissionRule, +} from './types.js'; + +/** + * 系统默认权限配置 + */ +export const SYSTEM_DEFAULT_PERMISSION: AgentPermission = { + file: { + read: 'allow', + write: 'ask', + edit: 'ask', + delete: 'ask', + }, + bash: { + enabled: true, + default: 'ask', + rules: [ + // 安全命令 + { pattern: 'ls *', action: 'allow' }, + { pattern: 'pwd', action: 'allow' }, + { pattern: 'cat *', action: 'allow' }, + { pattern: 'head *', action: 'allow' }, + { pattern: 'tail *', action: 'allow' }, + { pattern: 'wc *', action: 'allow' }, + { pattern: 'echo *', action: 'allow' }, + { pattern: 'which *', action: 'allow' }, + { pattern: 'type *', action: 'allow' }, + // 危险命令 + { pattern: 'rm -rf *', action: 'deny' }, + { pattern: 'rm -fr *', action: 'deny' }, + { pattern: 'sudo *', action: 'deny' }, + { pattern: 'chmod 777 *', action: 'deny' }, + ], + }, + web: 'ask', + git: { + read: 'allow', + write: 'ask', + dangerous: 'deny', + }, +}; + +/** + * 合并单个权限值 + * 优先级: agent > global > system + */ +function mergeAction( + system: PermissionAction | undefined, + global: PermissionAction | undefined, + agent: PermissionAction | undefined +): PermissionAction { + return agent ?? global ?? system ?? 'ask'; +} + +/** + * 合并规则数组 + * Agent 规则优先,然后是 global,最后是 system + */ +function mergeRules( + system: PermissionRule[] | undefined, + global: PermissionRule[] | undefined, + agent: PermissionRule[] | undefined +): PermissionRule[] { + return [ + ...(agent ?? []), + ...(global ?? []), + ...(system ?? []), + ]; +} + +/** + * 合并文件权限配置 + */ +function mergeFilePermission( + system: AgentFilePermission | undefined, + global: AgentFilePermission | undefined, + agent: AgentFilePermission | undefined +): AgentFilePermission { + return { + read: mergeAction(system?.read, global?.read, agent?.read), + write: mergeAction(system?.write, global?.write, agent?.write), + edit: mergeAction(system?.edit, global?.edit, agent?.edit), + delete: mergeAction(system?.delete, global?.delete, agent?.delete), + sensitivePaths: mergeRules( + system?.sensitivePaths, + global?.sensitivePaths, + agent?.sensitivePaths + ), + }; +} + +/** + * 合并 Bash 权限配置 + */ +function mergeBashPermission( + system: AgentBashPermission | undefined, + global: AgentBashPermission | undefined, + agent: AgentBashPermission | undefined +): AgentBashPermission { + // 如果 agent 显式禁用,直接返回 + if (agent?.enabled === false) { + return { enabled: false }; + } + + // 如果 global 禁用且 agent 没有覆盖 + if (global?.enabled === false && agent?.enabled === undefined) { + return { enabled: false }; + } + + return { + enabled: agent?.enabled ?? global?.enabled ?? system?.enabled ?? true, + rules: mergeRules(system?.rules, global?.rules, agent?.rules), + default: mergeAction(system?.default, global?.default, agent?.default), + }; +} + +/** + * 合并 Git 权限配置 + */ +function mergeGitPermission( + system: AgentGitPermission | undefined, + global: AgentGitPermission | undefined, + agent: AgentGitPermission | undefined +): AgentGitPermission { + return { + read: mergeAction(system?.read, global?.read, agent?.read), + write: mergeAction(system?.write, global?.write, agent?.write), + dangerous: mergeAction(system?.dangerous, global?.dangerous, agent?.dangerous), + }; +} + +/** + * 合并完整的权限配置 + * 优先级: Agent 配置 > 全局默认 > 系统默认 + */ +export function mergePermissions( + systemDefault: AgentPermission, + globalConfig: AgentPermission | undefined, + agentConfig: AgentPermission | undefined +): AgentPermission { + return { + file: mergeFilePermission( + systemDefault.file, + globalConfig?.file, + agentConfig?.file + ), + bash: mergeBashPermission( + systemDefault.bash, + globalConfig?.bash, + agentConfig?.bash + ), + web: mergeAction(systemDefault.web, globalConfig?.web, agentConfig?.web), + git: mergeGitPermission( + systemDefault.git, + globalConfig?.git, + agentConfig?.git + ), + }; +} + +/** + * 检查命令是否匹配规则 + * 支持通配符 * 匹配任意字符 + */ +export function matchRule(command: string, pattern: string): boolean { + // 将通配符模式转换为正则表达式 + const regexPattern = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义特殊字符 + .replace(/\*/g, '.*') // * 转换为 .* + .replace(/\?/g, '.'); // ? 转换为 . + + const regex = new RegExp(`^${regexPattern}$`, 'i'); + return regex.test(command); +} + +/** + * 根据规则列表检查命令权限 + */ +export function checkBashPermission( + command: string, + permission: AgentBashPermission +): PermissionAction { + // 如果禁用,直接拒绝 + if (permission.enabled === false) { + return 'deny'; + } + + // 按顺序检查规则 + for (const rule of permission.rules ?? []) { + if (matchRule(command, rule.pattern)) { + return rule.action; + } + } + + // 返回默认策略 + return permission.default ?? 'ask'; +} + +/** + * 根据规则列表检查文件路径权限 + */ +export function checkFilePathPermission( + filePath: string, + sensitivePaths: PermissionRule[] | undefined +): PermissionAction | null { + if (!sensitivePaths) return null; + + for (const rule of sensitivePaths) { + if (matchRule(filePath, rule.pattern)) { + return rule.action; + } + } + + return null; +} diff --git a/src/agent/presets/build.ts b/src/agent/presets/build.ts new file mode 100644 index 0000000..776b485 --- /dev/null +++ b/src/agent/presets/build.ts @@ -0,0 +1,11 @@ +import type { AgentInfo } from '../types.js'; + +/** + * 构建 Agent + * 主模式,拥有完整权限执行编码任务 + */ +export const buildAgent: Omit = { + description: '构建模式,拥有完整权限执行编码任务', + mode: 'primary', + maxSteps: 30, +}; diff --git a/src/agent/presets/code-reviewer.ts b/src/agent/presets/code-reviewer.ts new file mode 100644 index 0000000..7295b62 --- /dev/null +++ b/src/agent/presets/code-reviewer.ts @@ -0,0 +1,86 @@ +import type { AgentInfo } from '../types.js'; + +/** + * 代码审查 Agent + * 检查代码质量、安全性和最佳实践 + */ +export const codeReviewerAgent: Omit = { + description: '代码审查专家,检查代码质量、安全性和最佳实践', + mode: 'subagent', + prompt: `你是一个资深代码审查专家。你的任务是全面分析代码质量。 + +审查重点: +1. **代码质量** + - 可读性和命名规范 + - 代码结构和组织 + - 重复代码和可复用性 + - 注释和文档 + +2. **潜在 Bug** + - 边界条件处理 + - 空值/undefined 检查 + - 类型安全问题 + - 异步错误处理 + +3. **安全漏洞** + - SQL 注入 + - XSS 跨站脚本 + - 敏感信息泄露 + - 权限控制问题 + +4. **性能问题** + - 不必要的计算 + - 内存泄漏风险 + - N+1 查询问题 + - 大数据处理 + +5. **最佳实践** + - 设计模式应用 + - SOLID 原则 + - 错误处理策略 + - 测试覆盖建议 + +输出格式: +- 问题分为 Critical(严重)、Warning(警告)、Info(建议)三个等级 +- 每个问题说明:位置、描述、建议修复方案 +- 最后给出总体评分和改进建议`, + tools: { + enabled: [ + 'read_file', + 'list_directory', + 'search_files', + 'grep_content', + 'git_status', + 'git_diff', + ], + noTask: true, + }, + permission: { + file: { + read: 'allow', + write: 'deny', + edit: 'deny', + delete: 'deny', + }, + bash: { + enabled: true, + rules: [ + { pattern: 'npm run lint*', action: 'allow' }, + { pattern: 'npm run test*', action: 'allow' }, + { pattern: 'npx eslint *', action: 'allow' }, + { pattern: 'npx tsc --noEmit*', action: 'allow' }, + { pattern: 'tsc --noEmit*', action: 'allow' }, + ], + default: 'deny', + }, + git: { + read: 'allow', + write: 'deny', + dangerous: 'deny', + }, + }, + model: { + temperature: 0.3, // 低温度,更精确的分析 + }, + maxSteps: 10, +}; diff --git a/src/agent/presets/explore.ts b/src/agent/presets/explore.ts new file mode 100644 index 0000000..514e0d8 --- /dev/null +++ b/src/agent/presets/explore.ts @@ -0,0 +1,49 @@ +import type { AgentInfo } from '../types.js'; + +/** + * 探索 Agent + * 快速探索代码库,搜索文件和代码结构(只读) + */ +export const exploreAgent: Omit = { + description: '快速探索代码库,搜索文件和代码结构(只读)', + mode: 'subagent', + prompt: `你是一个代码探索专家。你的任务是快速搜索和理解代码库结构。 + +规则: +- 只做搜索和读取操作,禁止修改任何文件 +- 使用 search_files、grep_content、read_file、list_directory 工具来探索代码 +- 提供清晰、结构化的分析结果 +- 关注代码结构、依赖关系和关键实现 + +输出格式: +- 使用 Markdown 格式组织信息 +- 列出关键文件和它们的作用 +- 总结代码结构和设计模式`, + tools: { + enabled: [ + 'read_file', + 'list_directory', + 'search_files', + 'grep_content', + 'get_file_info', + ], + noTask: true, + }, + permission: { + file: { + read: 'allow', + write: 'deny', + edit: 'deny', + delete: 'deny', + }, + bash: { + enabled: false, + }, + git: { + read: 'allow', + write: 'deny', + dangerous: 'deny', + }, + }, + maxSteps: 20, +}; diff --git a/src/agent/presets/general.ts b/src/agent/presets/general.ts new file mode 100644 index 0000000..494be21 --- /dev/null +++ b/src/agent/presets/general.ts @@ -0,0 +1,14 @@ +import type { AgentInfo } from '../types.js'; + +/** + * 通用 Agent + * 适合复杂的多步骤任务、代码搜索和问题研究 + */ +export const generalAgent: Omit = { + description: '通用 Agent,适合复杂的多步骤任务、代码搜索和问题研究', + mode: 'subagent', + tools: { + noTask: true, // 禁止嵌套调用 Task + }, + maxSteps: 15, +}; diff --git a/src/agent/presets/index.ts b/src/agent/presets/index.ts new file mode 100644 index 0000000..ff2965d --- /dev/null +++ b/src/agent/presets/index.ts @@ -0,0 +1,33 @@ +import type { AgentInfo } from '../types.js'; +import { generalAgent } from './general.js'; +import { exploreAgent } from './explore.js'; +import { codeReviewerAgent } from './code-reviewer.js'; +import { buildAgent } from './build.js'; +import { planAgent } from './plan.js'; + +/** + * 预设 Agent 集合 + */ +export const presetAgents: Record> = { + general: generalAgent, + explore: exploreAgent, + 'code-reviewer': codeReviewerAgent, + build: buildAgent, + plan: planAgent, +}; + +/** + * 获取所有预设 Agent 名称 + */ +export function getPresetAgentNames(): string[] { + return Object.keys(presetAgents); +} + +/** + * 检查是否为预设 Agent + */ +export function isPresetAgent(name: string): boolean { + return name in presetAgents; +} + +export { generalAgent, exploreAgent, codeReviewerAgent, buildAgent, planAgent }; diff --git a/src/agent/presets/plan.ts b/src/agent/presets/plan.ts new file mode 100644 index 0000000..9aae8a3 --- /dev/null +++ b/src/agent/presets/plan.ts @@ -0,0 +1,82 @@ +import type { AgentInfo } from '../types.js'; + +/** + * 计划 Agent + * 主模式,设计实现方案(不执行修改) + */ +export const planAgent: Omit = { + description: '计划模式,设计实现方案(不执行修改)', + mode: 'primary', + prompt: `你是一个软件架构师。你的任务是设计实现方案,而不是直接执行。 + +工作流程: +1. 理解需求:分析用户的需求和目标 +2. 调研现状:阅读相关代码,了解现有架构 +3. 设计方案:提出详细的实现计划 +4. 评估风险:识别潜在问题和挑战 + +规则: +- 可以读取代码来了解现状 +- 只输出计划,不要实际执行修改 +- 提供具体、可操作的步骤 + +输出格式: +## 需求分析 +[对需求的理解] + +## 现状分析 +[相关代码的结构和设计] + +## 实现方案 +### 步骤 1: [标题] +- 目标: ... +- 涉及文件: ... +- 具体修改: ... + +### 步骤 2: [标题] +... + +## 风险评估 +- [风险1]: [应对方案] +- [风险2]: [应对方案] + +## 测试计划 +- [测试项1] +- [测试项2]`, + tools: { + disabled: [ + 'write_file', + 'edit_file', + 'delete_file', + 'move_file', + 'copy_file', + 'create_directory', + 'bash', + 'git_add', + 'git_commit', + 'git_push', + 'git_checkout', + 'git_stash', + ], + }, + permission: { + file: { + read: 'allow', + write: 'deny', + edit: 'deny', + delete: 'deny', + }, + bash: { + enabled: false, + }, + git: { + read: 'allow', + write: 'deny', + dangerous: 'deny', + }, + }, + model: { + temperature: 0.5, + }, + maxSteps: 15, +}; diff --git a/src/agent/registry.ts b/src/agent/registry.ts new file mode 100644 index 0000000..c355c8c --- /dev/null +++ b/src/agent/registry.ts @@ -0,0 +1,154 @@ +import type { AgentInfo, AgentConfigFile, AgentMode } from './types.js'; +import { presetAgents } from './presets/index.js'; +import { loadAgentConfig } from './config-loader.js'; +import { mergePermissions, SYSTEM_DEFAULT_PERMISSION } from './permission-merger.js'; + +/** + * Agent 注册表 + * 管理所有 Agent 的注册、查询和配置合并 + */ +export class AgentRegistry { + private agents: Map = new Map(); + private globalConfig: AgentConfigFile['defaults'] | null = null; + private userConfig: AgentConfigFile | null = null; + private initialized = false; + + /** + * 初始化 - 加载预设和用户配置 + */ + async init(workdir: string): Promise { + if (this.initialized) return; + + // 1. 注册预设 Agent + for (const [name, agentConfig] of Object.entries(presetAgents)) { + this.agents.set(name, { ...agentConfig, name }); + } + + // 2. 加载用户配置 + this.userConfig = await loadAgentConfig(workdir); + if (this.userConfig) { + this.globalConfig = this.userConfig.defaults ?? null; + + // 注册用户自定义 Agent + if (this.userConfig.agents) { + for (const [name, config] of Object.entries(this.userConfig.agents)) { + this.agents.set(name, { ...config, name }); + } + } + } + + this.initialized = true; + } + + /** + * 获取 Agent(应用权限合并) + */ + get(name: string): AgentInfo | undefined { + const agent = this.agents.get(name); + if (!agent) return undefined; + + return this.applyGlobalConfig(agent); + } + + /** + * 列出所有 Agent + */ + list(mode?: AgentMode): AgentInfo[] { + return [...this.agents.values()] + .filter((a) => !mode || a.mode === mode || a.mode === 'all') + .map((a) => this.applyGlobalConfig(a)); + } + + /** + * 列出可作为子 Agent 的 Agent(供 Task 工具使用) + */ + listSubagents(): AgentInfo[] { + return this.list().filter((a) => a.mode !== 'primary'); + } + + /** + * 动态注册 Agent(运行时) + */ + register(agent: AgentInfo): void { + this.agents.set(agent.name, agent); + } + + /** + * 移除 Agent + */ + remove(name: string): boolean { + return this.agents.delete(name); + } + + /** + * 检查 Agent 是否存在 + */ + has(name: string): boolean { + return this.agents.has(name); + } + + /** + * 获取 Agent 数量 + */ + get size(): number { + return this.agents.size; + } + + /** + * 获取所有 Agent 名称 + */ + getNames(): string[] { + return [...this.agents.keys()]; + } + + /** + * 获取全局配置 + */ + getGlobalConfig(): AgentConfigFile['defaults'] | null { + return this.globalConfig; + } + + /** + * 应用全局配置到 Agent + */ + private applyGlobalConfig(agent: AgentInfo): AgentInfo { + // 合并 maxSteps + const maxSteps = agent.maxSteps ?? this.globalConfig?.maxSteps ?? 10; + + // 合并模型配置 + const model = { + ...this.globalConfig?.model, + ...agent.model, + }; + + // 合并权限配置 + const permission = mergePermissions( + SYSTEM_DEFAULT_PERMISSION, + this.globalConfig?.permission, + agent.permission + ); + + return { + ...agent, + maxSteps, + model: Object.keys(model).length > 0 ? model : undefined, + permission, + }; + } + + /** + * 生成 Task 工具的 Agent 描述(用于工具 description) + */ + generateSubagentDescription(): string { + const subagents = this.listSubagents(); + if (subagents.length === 0) { + return '当前没有可用的子 Agent'; + } + + const descriptions = subagents.map((a) => `- ${a.name}: ${a.description}`); + return `可用的 Agent:\n${descriptions.join('\n')}`; + } +} + +// 导出单例 +export const agentRegistry = new AgentRegistry(); diff --git a/src/agent/types.ts b/src/agent/types.ts new file mode 100644 index 0000000..8868412 --- /dev/null +++ b/src/agent/types.ts @@ -0,0 +1,164 @@ +import type { ProviderType } from '../types/index.js'; + +/** + * Agent 模式 + * - primary: 主 Agent,用户直接使用 + * - subagent: 子 Agent,由 Task 工具调用 + * - all: 两种方式都可以 + */ +export type AgentMode = 'primary' | 'subagent' | 'all'; + +/** + * 权限动作 + */ +export type PermissionAction = 'allow' | 'deny' | 'ask'; + +/** + * 权限规则(支持通配符模式) + */ +export interface PermissionRule { + pattern: string; + action: PermissionAction; +} + +/** + * Agent Bash 权限配置 + */ +export interface AgentBashPermission { + /** 是否启用 bash */ + enabled?: boolean; + /** 命令规则,按顺序匹配(支持通配符如 "git diff*", "rm -rf*") */ + rules?: PermissionRule[]; + /** 默认策略 */ + default?: PermissionAction; +} + +/** + * Agent 文件权限配置 + */ +export interface AgentFilePermission { + read?: PermissionAction; + write?: PermissionAction; + edit?: PermissionAction; + delete?: PermissionAction; + /** 敏感路径规则 */ + sensitivePaths?: PermissionRule[]; +} + +/** + * Agent Git 权限配置 + */ +export interface AgentGitPermission { + /** 读操作(status, diff, log 等) */ + read?: PermissionAction; + /** 写操作(add, commit, push 等) */ + write?: PermissionAction; + /** 危险操作(force push, reset --hard 等) */ + dangerous?: PermissionAction; +} + +/** + * Agent 完整权限配置 + */ +export interface AgentPermission { + file?: AgentFilePermission; + bash?: AgentBashPermission; + web?: PermissionAction; + git?: AgentGitPermission; +} + +/** + * Agent 模型配置 + */ +export interface AgentModelConfig { + /** Provider 类型 */ + provider?: ProviderType; + /** 模型名称 */ + model?: string; + /** 温度参数 */ + temperature?: number; + /** Top P 参数 */ + topP?: number; + /** 最大输出 tokens */ + maxTokens?: number; +} + +/** + * Agent 工具配置 + */ +export interface AgentToolConfig { + /** 禁用的工具列表 */ + disabled?: string[]; + /** 启用的工具列表(如果设置,则只启用这些) */ + enabled?: string[]; + /** 禁止嵌套 Task */ + noTask?: boolean; +} + +/** + * Agent 定义 + */ +export interface AgentInfo { + /** Agent 名称 */ + name: string; + /** Agent 描述 */ + description: string; + /** Agent 模式 */ + mode: AgentMode; + /** 自定义 System Prompt */ + prompt?: string; + /** 模型配置 */ + model?: AgentModelConfig; + /** 工具配置 */ + tools?: AgentToolConfig; + /** 权限配置 */ + permission?: AgentPermission; + /** 最大执行步数 */ + maxSteps?: number; +} + +/** + * Agent 配置文件格式(用户自定义) + */ +export interface AgentConfigFile { + /** 全局默认配置 */ + defaults?: { + maxSteps?: number; + model?: AgentModelConfig; + permission?: AgentPermission; + }; + /** Agent 定义 */ + agents?: Record>; +} + +/** + * Agent 执行上下文 + */ +export interface AgentExecutionContext { + /** 父会话 ID */ + parentSessionId?: string; + /** 工作目录 */ + workdir: string; + /** 回调:输出流 */ + onStream?: (text: string) => void; + /** 回调:工具调用 */ + onToolCall?: (toolName: string, params: Record) => void; + /** 回调:工具结果 */ + onToolResult?: (toolName: string, result: unknown) => void; +} + +/** + * Agent 执行结果 + */ +export interface AgentExecutionResult { + /** 是否成功 */ + success: boolean; + /** 输出文本 */ + text: string; + /** 执行步数 */ + steps: number; + /** 会话 ID */ + sessionId: string; + /** 错误信息 */ + error?: string; +} diff --git a/src/index.ts b/src/index.ts index d16f7aa..10bf0ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,10 @@ import { Command } from 'commander'; import { Agent } from './core/agent.js'; import { TerminalUI } from './ui/terminal.js'; import { loadConfig, initConfig } from './utils/config.js'; -import { toolRegistry, todoManager } from './tools/index.js'; +import { toolRegistry, todoManager, initTaskContext, updateTaskDescription } from './tools/index.js'; import { getPermissionManager, promptPermission } from './permission/index.js'; import { SessionManager } from './session/index.js'; +import { agentRegistry } from './agent/index.js'; import { initLSP, shutdownLSP } from './lsp/index.js'; import { printServerList, @@ -119,6 +120,13 @@ program.action(async () => { // 初始化 todoManager(让 todo 工具可以访问会话) todoManager.setSessionManager(sessionManager); + // 初始化 Agent 注册表(加载预设和用户配置) + await agentRegistry.init(process.cwd()); + + // 初始化 Task 工具上下文 + initTaskContext(config, sessionManager); + updateTaskDescription(); + // 显示会话恢复信息 const session = sessionManager.getSession(); if (session && session.messages.length > 0) { diff --git a/src/session/manager.ts b/src/session/manager.ts index 0e915fa..d0c1def 100644 --- a/src/session/manager.ts +++ b/src/session/manager.ts @@ -145,6 +145,43 @@ export class SessionManager { return this.currentSession; } + /** + * 创建子会话(用于 Task 工具) + * @param parentId 父会话 ID + * @param agentName 关联的 Agent 名称 + * @param title 会话标题 + */ + createChildSession(parentId: string, agentName: string, title?: string): SessionData { + const workdir = this.currentSession?.workdir || process.cwd(); + const childSession: SessionData = { + id: this.storage.generateSessionId(), + parentId, + agentName, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + workdir, + title: title || `子任务 (@${agentName})`, + messages: [], + discoveredTools: [], + todos: [], + }; + return childSession; + } + + /** + * 保存子会话 + */ + async saveChildSession(session: SessionData): Promise { + await this.storage.saveSession(session); + } + + /** + * 获取当前会话 ID + */ + getSessionId(): string | undefined { + return this.currentSession?.id; + } + /** * 恢复指定会话 */ diff --git a/src/session/storage.ts b/src/session/storage.ts index 052f7a4..dd64f94 100644 --- a/src/session/storage.ts +++ b/src/session/storage.ts @@ -145,6 +145,16 @@ export class SessionStorage { } } + /** + * 保存指定会话(用于子会话) + */ + async saveSession(session: SessionData): Promise { + await this.ensureDir(); + session.updatedAt = new Date().toISOString(); + const filePath = path.join(this.sessionsDir, `${session.id}.json`); + await fs.writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8'); + } + /** * 删除指定会话 */ diff --git a/src/session/types.ts b/src/session/types.ts index 0b3167b..4628332 100644 --- a/src/session/types.ts +++ b/src/session/types.ts @@ -22,6 +22,10 @@ export interface Todo { export interface SessionData { /** 会话 ID */ id: string; + /** 父会话 ID(子会话时存在) */ + parentId?: string; + /** 关联的 Agent 名称(子会话时存在) */ + agentName?: string; /** 创建时间 */ createdAt: string; /** 最后更新时间 */ diff --git a/src/tools/index.ts b/src/tools/index.ts index e27f166..c56e305 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -8,6 +8,9 @@ import { bashTool } from './shell/index.js'; import { toolSearchTool } from './tool-search.js'; import { todoReadTool, todoWriteTool } from './todo/index.js'; +// Task 工具(Agent 子任务) +import { taskTool } from './task/index.js'; + // 文件系统工具 import { readFileTool, @@ -47,6 +50,7 @@ const allToolsWithMetadata: ToolWithMetadata[] = [ bashTool, todoReadTool, todoWriteTool, + taskTool, // 文件系统工具 (deferLoading: true) readFileTool, @@ -85,6 +89,7 @@ toolRegistry.registerAll(allToolsWithMetadata); export { toolRegistry } from './registry.js'; export { toolSearchTool } from './tool-search.js'; export { todoManager } from './todo/index.js'; +export { initTaskContext, updateTaskDescription } from './task/index.js'; export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js'; // 兼容旧代码:导出所有工具数组(基础 Tool 类型) diff --git a/src/tools/task/index.ts b/src/tools/task/index.ts new file mode 100644 index 0000000..8250da0 --- /dev/null +++ b/src/tools/task/index.ts @@ -0,0 +1 @@ +export { taskTool, initTaskContext, updateTaskDescription } from './task.js'; diff --git a/src/tools/task/task.ts b/src/tools/task/task.ts new file mode 100644 index 0000000..c46d241 --- /dev/null +++ b/src/tools/task/task.ts @@ -0,0 +1,171 @@ +import type { ToolWithMetadata } from '../types.js'; +import type { AgentConfig } from '../../types/index.js'; +import { agentRegistry, AgentExecutor } from '../../agent/index.js'; +import { toolRegistry } from '../registry.js'; +import { SessionManager } from '../../session/index.js'; + +// Task 工具上下文(运行时注入) +let taskContext: { + baseConfig: AgentConfig; + sessionManager: SessionManager; +} | null = null; + +/** + * 初始化 Task 工具上下文 + */ +export function initTaskContext( + baseConfig: AgentConfig, + sessionManager: SessionManager +): void { + taskContext = { baseConfig, sessionManager }; +} + +/** + * 获取 Task 工具动态描述 + */ +function getTaskDescription(): string { + const subagents = agentRegistry.listSubagents(); + if (subagents.length === 0) { + return '执行子任务(当前没有可用的子 Agent)'; + } + + const agentList = subagents + .map((a) => `- ${a.name}: ${a.description}`) + .join('\n'); + + return `执行子任务,委派给专门的 Agent 处理。 + +可用的 Agent: +${agentList} + +使用示例: +- 使用 explore Agent 搜索代码: task({ subagent_type: "explore", prompt: "找到所有 API 路由定义" }) +- 使用 code-reviewer Agent 审查: task({ subagent_type: "code-reviewer", prompt: "审查 src/auth 目录的代码" }) +- 使用 general Agent 执行复杂任务: task({ subagent_type: "general", prompt: "分析并重构这个函数" })`; +} + +/** + * Task 工具 + * 用于创建子任务,委派给指定的 Agent 处理 + */ +export const taskTool: ToolWithMetadata = { + name: 'task', + description: getTaskDescription(), + parameters: { + description: { + type: 'string', + description: '任务简短描述(3-5 个词,用于标识任务)', + required: true, + }, + prompt: { + type: 'string', + description: '详细的任务说明,包括目标、范围和期望输出', + required: true, + }, + subagent_type: { + type: 'string', + description: '子 Agent 类型,可选: general, explore, code-reviewer', + required: true, + }, + }, + metadata: { + name: 'task', + category: 'agent', + description: '执行子任务,委派给专门的 Agent', + keywords: ['task', 'agent', 'subagent', '子任务', '委派', '探索', '审查'], + deferLoading: false, // 核心工具,始终加载 + }, + async execute(params) { + const { description, prompt, subagent_type } = params as { + description: string; + prompt: string; + subagent_type: string; + }; + + // 检查上下文是否已初始化 + if (!taskContext) { + return { + success: false, + output: '', + error: 'Task 工具未初始化,请确保正确设置上下文', + }; + } + + const { baseConfig, sessionManager } = taskContext; + + // 1. 获取 Agent 配置 + const agent = agentRegistry.get(subagent_type); + if (!agent) { + const availableAgents = agentRegistry.listSubagents().map((a) => a.name); + return { + success: false, + output: '', + error: `未找到 Agent: ${subagent_type}。可用的 Agent: ${availableAgents.join(', ')}`, + }; + } + + // 检查是否为 primary 模式 + if (agent.mode === 'primary') { + return { + success: false, + output: '', + error: `Agent "${subagent_type}" 是 primary 模式,不能作为子任务调用`, + }; + } + + // 2. 创建子会话 + const parentSessionId = sessionManager.getSessionId() || 'standalone'; + const childSession = sessionManager.createChildSession( + parentSessionId, + agent.name, + `${description} (@${agent.name})` + ); + + // 3. 创建执行器 + const executor = new AgentExecutor(agent, baseConfig, toolRegistry); + + // 4. 执行任务 + const result = await executor.execute(prompt, { + parentSessionId, + workdir: process.cwd(), + onStream: undefined, // 子任务不使用流式输出 + }); + + // 5. 保存子会话 + childSession.messages = [ + { role: 'user', content: prompt }, + { role: 'assistant', content: result.text }, + ]; + await sessionManager.saveChildSession(childSession); + + if (result.success) { + return { + success: true, + output: result.text, + metadata: { + agent: agent.name, + sessionId: childSession.id, + steps: result.steps, + }, + }; + } else { + return { + success: false, + output: '', + error: result.error || '子任务执行失败', + metadata: { + agent: agent.name, + sessionId: childSession.id, + steps: result.steps, + }, + }; + } + }, +}; + +/** + * 更新 Task 工具描述(Agent 注册表初始化后调用) + */ +export function updateTaskDescription(): void { + (taskTool as { description: string }).description = getTaskDescription(); +} diff --git a/src/tools/types.ts b/src/tools/types.ts index 251efe7..a355292 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -1,7 +1,7 @@ import type { ToolParameter, ToolResult } from '../types/index.js'; // 工具类别 -export type ToolCategory = 'core' | 'filesystem' | 'shell' | 'git' | 'web' | 'database'; +export type ToolCategory = 'core' | 'filesystem' | 'shell' | 'git' | 'web' | 'database' | 'agent'; // 工具元数据 export interface ToolMetadata {