From 623b40185050005ed7de4fadfe0e885fee5ab12e Mon Sep 17 00:00:00 2001 From: lpf Date: Fri, 6 Mar 2026 18:45:43 +0800 Subject: [PATCH] Fix config reload and subagent config feedback --- cmd/clawgo/cmd_gateway.go | 184 ++++++++++++----------- pkg/agent/subagent_config_intent.go | 44 +++++- pkg/agent/subagent_config_intent_test.go | 90 ++++------- 3 files changed, 168 insertions(+), 150 deletions(-) diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index 647f082..c927976 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -154,8 +154,12 @@ func gatewayCmd() { } return out }) + reloadReqCh := make(chan struct{}, 1) registryServer.SetConfigAfterHook(func() { - _ = requestGatewayReloadSignal() + select { + case reloadReqCh <- struct{}{}: + default: + } }) registryServer.SetSubagentHandler(func(cctx context.Context, action string, args map[string]interface{}) (interface{}, error) { return agentLoop.HandleSubagentRuntime(cctx, action, args) @@ -309,77 +313,35 @@ func gatewayCmd() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, gatewayNotifySignals()...) - for { - sig := <-sigChan - switch { - case isGatewayReloadSignal(sig): - fmt.Println("\n↻ Reloading config...") - newCfg, err := config.LoadConfig(getConfigPath()) - if err != nil { - fmt.Printf("✗ Reload failed (load config): %v\n", err) - continue - } - if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") { - applyMaximumPermissionPolicy(newCfg) - } - configureCronServiceRuntime(cronService, newCfg) - heartbeatService.Stop() - heartbeatService = buildHeartbeatService(newCfg, msgBus) - if err := heartbeatService.Start(); err != nil { - fmt.Printf("Error starting heartbeat service: %v\n", err) - } + applyReload := func() { + fmt.Println("\n↻ Reloading config...") + newCfg, err := config.LoadConfig(getConfigPath()) + if err != nil { + fmt.Printf("✗ Reload failed (load config): %v\n", err) + return + } + if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") { + applyMaximumPermissionPolicy(newCfg) + } + configureCronServiceRuntime(cronService, newCfg) + heartbeatService.Stop() + heartbeatService = buildHeartbeatService(newCfg, msgBus) + if err := heartbeatService.Start(); err != nil { + fmt.Printf("Error starting heartbeat service: %v\n", err) + } - if reflect.DeepEqual(cfg, newCfg) { - fmt.Println("✓ Config unchanged, skip reload") - continue - } + if reflect.DeepEqual(cfg, newCfg) { + fmt.Println("✓ Config unchanged, skip reload") + return + } - runtimeSame := reflect.DeepEqual(cfg.Agents, newCfg.Agents) && - reflect.DeepEqual(cfg.Providers, newCfg.Providers) && - reflect.DeepEqual(cfg.Tools, newCfg.Tools) && - reflect.DeepEqual(cfg.Channels, newCfg.Channels) + runtimeSame := reflect.DeepEqual(cfg.Agents, newCfg.Agents) && + reflect.DeepEqual(cfg.Providers, newCfg.Providers) && + reflect.DeepEqual(cfg.Tools, newCfg.Tools) && + reflect.DeepEqual(cfg.Channels, newCfg.Channels) - if runtimeSame { - configureLogging(newCfg) - sentinelService.Stop() - sentinelService = sentinel.NewService( - getConfigPath(), - newCfg.WorkspacePath(), - newCfg.Sentinel.IntervalSec, - newCfg.Sentinel.AutoHeal, - func(message string) { - if newCfg.Sentinel.NotifyChannel != "" && newCfg.Sentinel.NotifyChatID != "" { - msgBus.PublishOutbound(bus.OutboundMessage{ - Channel: newCfg.Sentinel.NotifyChannel, - ChatID: newCfg.Sentinel.NotifyChatID, - Content: "[Sentinel] " + message, - }) - } - }, - ) - if newCfg.Sentinel.Enabled { - sentinelService.SetManager(channelManager) - sentinelService.Start() - } - cfg = newCfg - runtimecfg.Set(cfg) - fmt.Println("✓ Config hot-reload applied (logging/metadata only)") - continue - } - - newAgentLoop, newChannelManager, err := buildGatewayRuntime(ctx, newCfg, msgBus, cronService) - if err != nil { - fmt.Printf("✗ Reload failed (init runtime): %v\n", err) - continue - } - - channelManager.StopAll(ctx) - agentLoop.Stop() - - channelManager = newChannelManager - agentLoop = newAgentLoop - cfg = newCfg - runtimecfg.Set(cfg) + if runtimeSame { + configureLogging(newCfg) sentinelService.Stop() sentinelService = sentinel.NewService( getConfigPath(), @@ -397,27 +359,77 @@ func gatewayCmd() { }, ) if newCfg.Sentinel.Enabled { + sentinelService.SetManager(channelManager) sentinelService.Start() } - sentinelService.SetManager(channelManager) - - if err := channelManager.StartAll(ctx); err != nil { - fmt.Printf("✗ Reload failed (start channels): %v\n", err) - continue - } - go agentLoop.Run(ctx) - fmt.Println("✓ Config hot-reload applied") - default: - fmt.Println("\nShutting down...") - cancel() - heartbeatService.Stop() - sentinelService.Stop() - cronService.Stop() - agentLoop.Stop() - channelManager.StopAll(ctx) - fmt.Println("✓ Gateway stopped") + cfg = newCfg + runtimecfg.Set(cfg) + fmt.Println("✓ Config hot-reload applied (logging/metadata only)") return } + + newAgentLoop, newChannelManager, err := buildGatewayRuntime(ctx, newCfg, msgBus, cronService) + if err != nil { + fmt.Printf("✗ Reload failed (init runtime): %v\n", err) + return + } + + channelManager.StopAll(ctx) + agentLoop.Stop() + + channelManager = newChannelManager + agentLoop = newAgentLoop + cfg = newCfg + runtimecfg.Set(cfg) + sentinelService.Stop() + sentinelService = sentinel.NewService( + getConfigPath(), + newCfg.WorkspacePath(), + newCfg.Sentinel.IntervalSec, + newCfg.Sentinel.AutoHeal, + func(message string) { + if newCfg.Sentinel.NotifyChannel != "" && newCfg.Sentinel.NotifyChatID != "" { + msgBus.PublishOutbound(bus.OutboundMessage{ + Channel: newCfg.Sentinel.NotifyChannel, + ChatID: newCfg.Sentinel.NotifyChatID, + Content: "[Sentinel] " + message, + }) + } + }, + ) + if newCfg.Sentinel.Enabled { + sentinelService.Start() + } + sentinelService.SetManager(channelManager) + + if err := channelManager.StartAll(ctx); err != nil { + fmt.Printf("✗ Reload failed (start channels): %v\n", err) + return + } + go agentLoop.Run(ctx) + fmt.Println("✓ Config hot-reload applied") + } + + for { + select { + case <-reloadReqCh: + applyReload() + case sig := <-sigChan: + switch { + case isGatewayReloadSignal(sig): + applyReload() + default: + fmt.Println("\nShutting down...") + cancel() + heartbeatService.Stop() + sentinelService.Stop() + cronService.Stop() + agentLoop.Stop() + channelManager.StopAll(ctx) + fmt.Println("✓ Gateway stopped") + return + } + } } } diff --git a/pkg/agent/subagent_config_intent.go b/pkg/agent/subagent_config_intent.go index ed1935c..94a74c0 100644 --- a/pkg/agent/subagent_config_intent.go +++ b/pkg/agent/subagent_config_intent.go @@ -88,15 +88,49 @@ func extractSubagentDescription(content string) string { } func formatCreatedSubagentForUser(result map[string]interface{}, configPath string) string { + subagent, _ := result["subagent"].(map[string]interface{}) + role := "" + displayName := "" + toolAllowlist := interface{}(nil) + systemPromptFile := "" + if subagent != nil { + if v, _ := subagent["role"].(string); v != "" { + role = v + } + if v, _ := subagent["display_name"].(string); v != "" { + displayName = v + } + if tools, ok := subagent["tools"].(map[string]interface{}); ok { + toolAllowlist = tools["allowlist"] + } + if v, _ := subagent["system_prompt_file"].(string); v != "" { + systemPromptFile = v + } + } + routingKeywords := interface{}(nil) + if rules, ok := result["rules"].([]interface{}); ok { + agentID, _ := result["agent_id"].(string) + for _, raw := range rules { + rule, ok := raw.(map[string]interface{}) + if !ok { + continue + } + if strings.TrimSpace(fmt.Sprint(rule["agent_id"])) != agentID { + continue + } + routingKeywords = rule["keywords"] + break + } + } return fmt.Sprintf( "subagent 已写入 config.json。\npath: %s\nagent_id: %v\nrole: %v\ndisplay_name: %v\ntool_allowlist: %v\nrouting_keywords: %v\nsystem_prompt_file: %v", configPath, result["agent_id"], - result["role"], - result["display_name"], - result["tool_allowlist"], - result["routing_keywords"], - result["system_prompt_file"], + role, + displayName, + toolAllowlist, + routingKeywords, + systemPromptFile, ) } diff --git a/pkg/agent/subagent_config_intent_test.go b/pkg/agent/subagent_config_intent_test.go index 85703e4..c7587c8 100644 --- a/pkg/agent/subagent_config_intent_test.go +++ b/pkg/agent/subagent_config_intent_test.go @@ -1,71 +1,43 @@ package agent import ( - "context" - "path/filepath" "strings" "testing" - - "clawgo/pkg/bus" - "clawgo/pkg/config" - "clawgo/pkg/runtimecfg" ) -func TestMaybeHandleSubagentConfigIntentCreatePersistsImmediately(t *testing.T) { - workspace := t.TempDir() - configPath := filepath.Join(workspace, "config.json") - cfg := config.DefaultConfig() - cfg.Agents.Router.Enabled = true - cfg.Agents.Subagents["main"] = config.SubagentConfig{ - Enabled: true, - Type: "router", - Role: "orchestrator", - SystemPromptFile: "agents/main/AGENT.md", - } - if err := config.SaveConfig(configPath, cfg); err != nil { - t.Fatalf("save config failed: %v", err) - } - runtimecfg.Set(cfg) - t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) }) +func TestFormatCreatedSubagentForUserReadsNestedFields(t *testing.T) { + t.Parallel() - loop := &AgentLoop{configPath: configPath} - out, handled, err := loop.maybeHandleSubagentConfigIntent(context.Background(), bus.InboundMessage{ - SessionKey: "main", - Channel: "cli", - Content: "创建一个负责回归测试和验证修复结果的子代理", - }) - if err != nil { - t.Fatalf("create subagent failed: %v", err) - } - if !handled || !strings.Contains(out, "已写入 config.json") { - t.Fatalf("expected immediate persist response, got handled=%v out=%q", handled, out) - } - if !strings.Contains(out, configPath) { - t.Fatalf("expected response to include config path, got %q", out) - } + out := formatCreatedSubagentForUser(map[string]interface{}{ + "agent_id": "coder", + "subagent": map[string]interface{}{ + "role": "coding", + "display_name": "Code Agent", + "system_prompt_file": "agents/coder/AGENT.md", + "tools": map[string]interface{}{ + "allowlist": []interface{}{"filesystem", "shell"}, + }, + }, + "rules": []interface{}{ + map[string]interface{}{ + "agent_id": "coder", + "keywords": []interface{}{"code", "fix"}, + }, + }, + }, "/tmp/config.json") - reloaded, err := config.LoadConfig(configPath) - if err != nil { - t.Fatalf("reload config failed: %v", err) - } - if _, ok := reloaded.Agents.Subagents["tester"]; !ok { - t.Fatalf("expected tester subagent to persist, got %+v", reloaded.Agents.Subagents) - } -} - -func TestMaybeHandleSubagentConfigIntentConfirmCancelNoLongerHandled(t *testing.T) { - loop := &AgentLoop{} - for _, content := range []string{"确认创建", "取消创建"} { - out, handled, err := loop.maybeHandleSubagentConfigIntent(context.Background(), bus.InboundMessage{ - SessionKey: "main", - Channel: "cli", - Content: content, - }) - if err != nil { - t.Fatalf("unexpected error for %q: %v", content, err) - } - if handled || out != "" { - t.Fatalf("expected %q to pass through, got handled=%v out=%q", content, handled, out) + for _, want := range []string{ + "agent_id: coder", + "role: coding", + "display_name: Code Agent", + "system_prompt_file: agents/coder/AGENT.md", + "routing_keywords: [code fix]", + } { + if !strings.Contains(out, want) { + t.Fatalf("expected output to contain %q, got:\n%s", want, out) } } + if strings.Contains(out, "") { + t.Fatalf("did not expect nil placeholders, got:\n%s", out) + } }