package tools import ( "encoding/json" "fmt" "strings" "clawgo/pkg/config" "clawgo/pkg/configops" "clawgo/pkg/runtimecfg" ) func DraftConfigSubagent(description, agentIDHint string) map[string]interface{} { desc := strings.TrimSpace(description) lower := strings.ToLower(desc) role := inferDraftRole(lower) agentID := strings.TrimSpace(agentIDHint) if agentID == "" { agentID = inferDraftAgentID(role, lower) } displayName := inferDraftDisplayName(role, agentID) toolAllowlist := inferDraftToolAllowlist(role) keywords := inferDraftKeywords(role, lower) systemPrompt := inferDraftSystemPrompt(role, desc) return map[string]interface{}{ "agent_id": agentID, "role": role, "display_name": displayName, "description": desc, "notify_main_policy": "final_only", "system_prompt": systemPrompt, "system_prompt_file": "agents/" + agentID + "/AGENT.md", "memory_namespace": agentID, "tool_allowlist": toolAllowlist, "routing_keywords": keywords, "type": "worker", } } func UpsertConfigSubagent(configPath string, args map[string]interface{}) (map[string]interface{}, error) { configPath = strings.TrimSpace(configPath) if configPath == "" { return nil, fmt.Errorf("config path not configured") } agentID := stringArgFromMap(args, "agent_id") if agentID == "" { return nil, fmt.Errorf("agent_id is required") } cfg, err := config.LoadConfig(configPath) if err != nil { return nil, err } mainID := strings.TrimSpace(cfg.Agents.Router.MainAgentID) if mainID == "" { mainID = "main" } if cfg.Agents.Subagents == nil { cfg.Agents.Subagents = map[string]config.SubagentConfig{} } subcfg := cfg.Agents.Subagents[agentID] if enabled, ok := boolArgFromMap(args, "enabled"); ok { if agentID == mainID && !enabled { return nil, fmt.Errorf("main agent %q cannot be disabled", agentID) } subcfg.Enabled = enabled } else if !subcfg.Enabled { subcfg.Enabled = true } if v := stringArgFromMap(args, "role"); v != "" { subcfg.Role = v } if v := stringArgFromMap(args, "transport"); v != "" { subcfg.Transport = v } if v := stringArgFromMap(args, "node_id"); v != "" { subcfg.NodeID = v } if v := stringArgFromMap(args, "parent_agent_id"); v != "" { subcfg.ParentAgentID = v } if v := stringArgFromMap(args, "display_name"); v != "" { subcfg.DisplayName = v } if v := stringArgFromMap(args, "notify_main_policy"); v != "" { subcfg.NotifyMainPolicy = v } if v := stringArgFromMap(args, "description"); v != "" { subcfg.Description = v } if v := stringArgFromMap(args, "system_prompt"); v != "" { subcfg.SystemPrompt = v } if v := stringArgFromMap(args, "system_prompt_file"); v != "" { subcfg.SystemPromptFile = v } if v := stringArgFromMap(args, "memory_namespace"); v != "" { subcfg.MemoryNamespace = v } if items := stringListArgFromMap(args, "tool_allowlist"); len(items) > 0 { subcfg.Tools.Allowlist = items } if v := stringArgFromMap(args, "type"); v != "" { subcfg.Type = v } else if strings.TrimSpace(subcfg.Type) == "" { subcfg.Type = "worker" } if strings.TrimSpace(subcfg.Transport) == "" { subcfg.Transport = "local" } if subcfg.Enabled && strings.TrimSpace(subcfg.Transport) != "node" && strings.TrimSpace(subcfg.SystemPromptFile) == "" { return nil, fmt.Errorf("system_prompt_file is required for enabled agent %q", agentID) } cfg.Agents.Subagents[agentID] = subcfg if kws := stringListArgFromMap(args, "routing_keywords"); len(kws) > 0 { cfg.Agents.Router.Rules = upsertRouteRuleConfig(cfg.Agents.Router.Rules, config.AgentRouteRule{ AgentID: agentID, Keywords: kws, }) } if errs := config.Validate(cfg); len(errs) > 0 { return nil, fmt.Errorf("config validation failed: %v", errs[0]) } data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return nil, err } if _, err := configops.WriteConfigAtomicWithBackup(configPath, data); err != nil { return nil, err } runtimecfg.Set(cfg) return map[string]interface{}{ "ok": true, "agent_id": agentID, "subagent": cfg.Agents.Subagents[agentID], "rules": cfg.Agents.Router.Rules, }, nil } func DeleteConfigSubagent(configPath, agentID string) (map[string]interface{}, error) { configPath = strings.TrimSpace(configPath) if configPath == "" { return nil, fmt.Errorf("config path not configured") } agentID = strings.TrimSpace(agentID) if agentID == "" { return nil, fmt.Errorf("agent_id is required") } cfg, err := config.LoadConfig(configPath) if err != nil { return nil, err } mainID := strings.TrimSpace(cfg.Agents.Router.MainAgentID) if mainID == "" { mainID = "main" } if agentID == mainID { return nil, fmt.Errorf("main agent %q cannot be deleted", agentID) } if cfg.Agents.Subagents == nil { return map[string]interface{}{"ok": false, "found": false, "agent_id": agentID}, nil } if _, ok := cfg.Agents.Subagents[agentID]; !ok { return map[string]interface{}{"ok": false, "found": false, "agent_id": agentID}, nil } delete(cfg.Agents.Subagents, agentID) cfg.Agents.Router.Rules = removeRouteRuleConfig(cfg.Agents.Router.Rules, agentID) if errs := config.Validate(cfg); len(errs) > 0 { return nil, fmt.Errorf("config validation failed: %v", errs[0]) } data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return nil, err } if _, err := configops.WriteConfigAtomicWithBackup(configPath, data); err != nil { return nil, err } runtimecfg.Set(cfg) return map[string]interface{}{ "ok": true, "found": true, "agent_id": agentID, "rules": cfg.Agents.Router.Rules, }, nil } func stringArgFromMap(args map[string]interface{}, key string) string { if args == nil { return "" } v, _ := args[key].(string) return strings.TrimSpace(v) } func boolArgFromMap(args map[string]interface{}, key string) (bool, bool) { if args == nil { return false, false } raw, ok := args[key] if !ok { return false, false } switch v := raw.(type) { case bool: return v, true default: return false, false } } func stringListArgFromMap(args map[string]interface{}, key string) []string { if args == nil { return nil } raw, ok := args[key] if !ok { return nil } switch v := raw.(type) { case []string: return normalizeKeywords(v) case []interface{}: items := make([]string, 0, len(v)) for _, item := range v { s, _ := item.(string) s = strings.TrimSpace(s) if s != "" { items = append(items, s) } } return normalizeKeywords(items) default: return nil } } func upsertRouteRuleConfig(rules []config.AgentRouteRule, rule config.AgentRouteRule) []config.AgentRouteRule { agentID := strings.TrimSpace(rule.AgentID) if agentID == "" { return rules } rule.Keywords = normalizeKeywords(rule.Keywords) if len(rule.Keywords) == 0 { return rules } out := make([]config.AgentRouteRule, 0, len(rules)+1) replaced := false for _, existing := range rules { if strings.TrimSpace(existing.AgentID) == agentID { out = append(out, rule) replaced = true continue } out = append(out, existing) } if !replaced { out = append(out, rule) } return out } func removeRouteRuleConfig(rules []config.AgentRouteRule, agentID string) []config.AgentRouteRule { agentID = strings.TrimSpace(agentID) if agentID == "" { return rules } out := make([]config.AgentRouteRule, 0, len(rules)) for _, existing := range rules { if strings.TrimSpace(existing.AgentID) == agentID { continue } out = append(out, existing) } return out } func normalizeKeywords(items []string) []string { seen := map[string]struct{}{} out := make([]string, 0, len(items)) for _, item := range items { item = strings.ToLower(strings.TrimSpace(item)) if item == "" { continue } if _, ok := seen[item]; ok { continue } seen[item] = struct{}{} out = append(out, item) } return out } func inferDraftRole(lower string) string { switch { case containsDraftKeyword(lower, "test", "regression", "qa", "回归", "测试", "验证"): return "testing" case containsDraftKeyword(lower, "doc", "docs", "readme", "文档", "说明"): return "docs" case containsDraftKeyword(lower, "research", "investigate", "analyze", "调研", "分析", "研究"): return "research" default: return "coding" } } func inferDraftAgentID(role, lower string) string { switch role { case "testing": if containsDraftKeyword(lower, "review", "审查", "reviewer") { return "reviewer" } return "tester" case "docs": return "doc_writer" case "research": return "researcher" default: if containsDraftKeyword(lower, "frontend", "ui", "前端") { return "frontend-coder" } if containsDraftKeyword(lower, "backend", "api", "后端") { return "backend-coder" } return "coder" } } func inferDraftDisplayName(role, agentID string) string { switch role { case "testing": return "Test Agent" case "docs": return "Docs Agent" case "research": return "Research Agent" default: if strings.Contains(agentID, "frontend") { return "Frontend Code Agent" } if strings.Contains(agentID, "backend") { return "Backend Code Agent" } return "Code Agent" } } func inferDraftToolAllowlist(role string) []string { switch role { case "testing": return []string{"shell", "filesystem", "process_manager", "sessions"} case "docs": return []string{"filesystem", "read_file", "write_file", "edit_file", "repo_map", "sessions"} case "research": return []string{"web_search", "web_fetch", "repo_map", "sessions", "memory_search"} default: return []string{"filesystem", "shell", "repo_map", "sessions"} } } func inferDraftKeywords(role, lower string) []string { seed := []string{} switch role { case "testing": seed = []string{"test", "regression", "verify", "回归", "测试", "验证"} case "docs": seed = []string{"docs", "readme", "document", "文档", "说明"} case "research": seed = []string{"research", "analyze", "investigate", "调研", "分析", "研究"} default: seed = []string{"code", "implement", "fix", "refactor", "代码", "实现", "修复", "重构"} } if containsDraftKeyword(lower, "frontend", "前端", "ui") { seed = append(seed, "frontend", "ui", "前端") } if containsDraftKeyword(lower, "backend", "后端", "api") { seed = append(seed, "backend", "api", "后端") } return normalizeKeywords(seed) } func inferDraftSystemPrompt(role, description string) string { switch role { case "testing": return "你负责测试、验证、回归检查与风险反馈。任务描述:" + description case "docs": return "你负责文档编写、结构整理和说明补全。任务描述:" + description case "research": return "你负责调研、分析、比较方案,并输出结论与依据。任务描述:" + description default: return "你负责代码实现与重构,输出具体修改建议和变更结果。任务描述:" + description } } func containsDraftKeyword(text string, items ...string) bool { for _, item := range items { if strings.Contains(text, strings.ToLower(strings.TrimSpace(item))) { return true } } return false }