mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 12:07:28 +08:00
Fix config reload and subagent config feedback
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "<nil>") {
|
||||
t.Fatalf("did not expect nil placeholders, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user