mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 23:17:30 +08:00
Unify agent topology and subagent memory logging
This commit is contained in:
@@ -19,6 +19,9 @@ type SubagentTask struct {
|
||||
Label string `json:"label"`
|
||||
Role string `json:"role"`
|
||||
AgentID string `json:"agent_id"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
||||
SessionKey string `json:"session_key"`
|
||||
MemoryNS string `json:"memory_ns"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
@@ -165,6 +168,9 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
|
||||
memoryNS := agentID
|
||||
systemPrompt := ""
|
||||
systemPromptFile := ""
|
||||
transport := "local"
|
||||
nodeID := ""
|
||||
parentAgentID := ""
|
||||
toolAllowlist := []string(nil)
|
||||
maxRetries := 0
|
||||
retryBackoff := 1000
|
||||
@@ -191,6 +197,12 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
|
||||
if ns := normalizeSubagentIdentifier(profile.MemoryNamespace); ns != "" {
|
||||
memoryNS = ns
|
||||
}
|
||||
transport = strings.TrimSpace(profile.Transport)
|
||||
if transport == "" {
|
||||
transport = "local"
|
||||
}
|
||||
nodeID = strings.TrimSpace(profile.NodeID)
|
||||
parentAgentID = strings.TrimSpace(profile.ParentAgentID)
|
||||
systemPrompt = strings.TrimSpace(profile.SystemPrompt)
|
||||
systemPromptFile = strings.TrimSpace(profile.SystemPromptFile)
|
||||
toolAllowlist = append([]string(nil), profile.ToolAllowlist...)
|
||||
@@ -265,6 +277,9 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
|
||||
Label: label,
|
||||
Role: role,
|
||||
AgentID: agentID,
|
||||
Transport: transport,
|
||||
NodeID: nodeID,
|
||||
ParentAgentID: parentAgentID,
|
||||
SessionKey: sessionKey,
|
||||
MemoryNS: memoryNS,
|
||||
SystemPrompt: systemPrompt,
|
||||
|
||||
@@ -68,6 +68,15 @@ func UpsertConfigSubagent(configPath string, args map[string]interface{}) (map[s
|
||||
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
|
||||
}
|
||||
@@ -91,7 +100,10 @@ func UpsertConfigSubagent(configPath string, args map[string]interface{}) (map[s
|
||||
} else if strings.TrimSpace(subcfg.Type) == "" {
|
||||
subcfg.Type = "worker"
|
||||
}
|
||||
if subcfg.Enabled && strings.TrimSpace(subcfg.SystemPromptFile) == "" {
|
||||
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
|
||||
|
||||
@@ -20,7 +20,7 @@ func NewSubagentConfigTool(configPath string) *SubagentConfigTool {
|
||||
func (t *SubagentConfigTool) Name() string { return "subagent_config" }
|
||||
|
||||
func (t *SubagentConfigTool) Description() string {
|
||||
return "Draft or persist subagent config and router rules into config.json."
|
||||
return "Persist subagent config and router rules into config.json."
|
||||
}
|
||||
|
||||
func (t *SubagentConfigTool) Parameters() map[string]interface{} {
|
||||
@@ -29,17 +29,20 @@ func (t *SubagentConfigTool) Parameters() map[string]interface{} {
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "draft|upsert",
|
||||
"description": "upsert",
|
||||
},
|
||||
"description": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Natural-language worker description for draft or upsert.",
|
||||
"description": "Natural-language worker description used by callers before upsert.",
|
||||
},
|
||||
"agent_id_hint": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Optional preferred agent id seed for draft.",
|
||||
"description": "Optional preferred agent id seed used by callers before upsert.",
|
||||
},
|
||||
"agent_id": map[string]interface{}{"type": "string"},
|
||||
"transport": map[string]interface{}{"type": "string"},
|
||||
"node_id": map[string]interface{}{"type": "string"},
|
||||
"parent_agent_id": map[string]interface{}{"type": "string"},
|
||||
"role": map[string]interface{}{"type": "string"},
|
||||
"display_name": map[string]interface{}{"type": "string"},
|
||||
"system_prompt": map[string]interface{}{"type": "string"},
|
||||
@@ -68,14 +71,6 @@ func (t *SubagentConfigTool) SetConfigPath(path string) {
|
||||
func (t *SubagentConfigTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
|
||||
_ = ctx
|
||||
switch stringArgFromMap(args, "action") {
|
||||
case "draft":
|
||||
description := stringArgFromMap(args, "description")
|
||||
if description == "" {
|
||||
return "", fmt.Errorf("description is required")
|
||||
}
|
||||
return marshalSubagentConfigPayload(map[string]interface{}{
|
||||
"draft": DraftConfigSubagent(description, stringArgFromMap(args, "agent_id_hint")),
|
||||
})
|
||||
case "upsert":
|
||||
result, err := UpsertConfigSubagent(t.getConfigPath(), cloneSubagentConfigArgs(args))
|
||||
if err != nil {
|
||||
|
||||
@@ -10,28 +10,6 @@ import (
|
||||
"clawgo/pkg/runtimecfg"
|
||||
)
|
||||
|
||||
func TestSubagentConfigToolDraft(t *testing.T) {
|
||||
tool := NewSubagentConfigTool("")
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"action": "draft",
|
||||
"description": "创建一个负责回归测试和验证修复结果的子代理",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("draft failed: %v", err)
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
||||
t.Fatalf("unmarshal payload failed: %v", err)
|
||||
}
|
||||
draft, ok := payload["draft"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected draft map, got %#v", payload["draft"])
|
||||
}
|
||||
if draft["agent_id"] == "" || draft["role"] == "" {
|
||||
t.Fatalf("expected draft agent_id and role, got %#v", draft)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubagentConfigToolUpsert(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
configPath := filepath.Join(workspace, "config.json")
|
||||
|
||||
@@ -12,12 +12,16 @@ import (
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/nodes"
|
||||
"clawgo/pkg/runtimecfg"
|
||||
)
|
||||
|
||||
type SubagentProfile struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Name string `json:"name"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
SystemPromptFile string `json:"system_prompt_file,omitempty"`
|
||||
@@ -122,6 +126,9 @@ func (s *SubagentProfileStore) Upsert(profile SubagentProfile) (*SubagentProfile
|
||||
if managed, ok := s.configProfileLocked(p.AgentID); ok {
|
||||
return nil, fmt.Errorf("subagent profile %q is managed by %s", p.AgentID, managed.ManagedBy)
|
||||
}
|
||||
if managed, ok := s.nodeProfileLocked(p.AgentID); ok {
|
||||
return nil, fmt.Errorf("subagent profile %q is managed by %s", p.AgentID, managed.ManagedBy)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
path := s.profilePath(p.AgentID)
|
||||
@@ -160,6 +167,9 @@ func (s *SubagentProfileStore) Delete(agentID string) error {
|
||||
if managed, ok := s.configProfileLocked(id); ok {
|
||||
return fmt.Errorf("subagent profile %q is managed by %s", id, managed.ManagedBy)
|
||||
}
|
||||
if managed, ok := s.nodeProfileLocked(id); ok {
|
||||
return fmt.Errorf("subagent profile %q is managed by %s", id, managed.ManagedBy)
|
||||
}
|
||||
|
||||
err := os.Remove(s.profilePath(id))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
@@ -175,6 +185,9 @@ func normalizeSubagentProfile(in SubagentProfile) SubagentProfile {
|
||||
if p.Name == "" {
|
||||
p.Name = p.AgentID
|
||||
}
|
||||
p.Transport = normalizeProfileTransport(p.Transport)
|
||||
p.NodeID = strings.TrimSpace(p.NodeID)
|
||||
p.ParentAgentID = normalizeSubagentIdentifier(p.ParentAgentID)
|
||||
p.Role = strings.TrimSpace(p.Role)
|
||||
p.SystemPrompt = strings.TrimSpace(p.SystemPrompt)
|
||||
p.SystemPromptFile = strings.TrimSpace(p.SystemPromptFile)
|
||||
@@ -203,6 +216,17 @@ func normalizeProfileStatus(s string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeProfileTransport(s string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||
case "", "local":
|
||||
return "local"
|
||||
case "node":
|
||||
return "node"
|
||||
default:
|
||||
return "local"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeStringList(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
@@ -266,6 +290,12 @@ func (s *SubagentProfileStore) mergedProfilesLocked() (map[string]SubagentProfil
|
||||
for _, p := range s.configProfilesLocked() {
|
||||
merged[p.AgentID] = p
|
||||
}
|
||||
for _, p := range s.nodeProfilesLocked() {
|
||||
if _, exists := merged[p.AgentID]; exists {
|
||||
continue
|
||||
}
|
||||
merged[p.AgentID] = p
|
||||
}
|
||||
fileProfiles, err := s.fileProfilesLocked()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -339,6 +369,27 @@ func (s *SubagentProfileStore) configProfileLocked(agentID string) (SubagentProf
|
||||
return profileFromConfig(id, subcfg), true
|
||||
}
|
||||
|
||||
func (s *SubagentProfileStore) nodeProfileLocked(agentID string) (SubagentProfile, bool) {
|
||||
id := normalizeSubagentIdentifier(agentID)
|
||||
if id == "" {
|
||||
return SubagentProfile{}, false
|
||||
}
|
||||
cfg := runtimecfg.Get()
|
||||
parentAgentID := "main"
|
||||
if cfg != nil {
|
||||
if mainID := normalizeSubagentIdentifier(cfg.Agents.Router.MainAgentID); mainID != "" {
|
||||
parentAgentID = mainID
|
||||
}
|
||||
}
|
||||
for _, node := range nodes.DefaultManager().List() {
|
||||
profile := profileFromNode(node, parentAgentID)
|
||||
if profile.AgentID == id {
|
||||
return profile, true
|
||||
}
|
||||
}
|
||||
return SubagentProfile{}, false
|
||||
}
|
||||
|
||||
func profileFromConfig(agentID string, subcfg config.SubagentConfig) SubagentProfile {
|
||||
status := "active"
|
||||
if !subcfg.Enabled {
|
||||
@@ -347,6 +398,9 @@ func profileFromConfig(agentID string, subcfg config.SubagentConfig) SubagentPro
|
||||
return normalizeSubagentProfile(SubagentProfile{
|
||||
AgentID: agentID,
|
||||
Name: strings.TrimSpace(subcfg.DisplayName),
|
||||
Transport: strings.TrimSpace(subcfg.Transport),
|
||||
NodeID: strings.TrimSpace(subcfg.NodeID),
|
||||
ParentAgentID: strings.TrimSpace(subcfg.ParentAgentID),
|
||||
Role: strings.TrimSpace(subcfg.Role),
|
||||
SystemPrompt: strings.TrimSpace(subcfg.SystemPrompt),
|
||||
SystemPromptFile: strings.TrimSpace(subcfg.SystemPromptFile),
|
||||
@@ -362,6 +416,63 @@ func profileFromConfig(agentID string, subcfg config.SubagentConfig) SubagentPro
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SubagentProfileStore) nodeProfilesLocked() []SubagentProfile {
|
||||
nodeItems := nodes.DefaultManager().List()
|
||||
if len(nodeItems) == 0 {
|
||||
return nil
|
||||
}
|
||||
cfg := runtimecfg.Get()
|
||||
parentAgentID := "main"
|
||||
if cfg != nil {
|
||||
if mainID := normalizeSubagentIdentifier(cfg.Agents.Router.MainAgentID); mainID != "" {
|
||||
parentAgentID = mainID
|
||||
}
|
||||
}
|
||||
out := make([]SubagentProfile, 0, len(nodeItems))
|
||||
for _, node := range nodeItems {
|
||||
profile := profileFromNode(node, parentAgentID)
|
||||
if profile.AgentID == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, profile)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func profileFromNode(node nodes.NodeInfo, parentAgentID string) SubagentProfile {
|
||||
agentID := nodeBranchAgentID(node.ID)
|
||||
if agentID == "" {
|
||||
return SubagentProfile{}
|
||||
}
|
||||
name := strings.TrimSpace(node.Name)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(node.ID)
|
||||
}
|
||||
status := "active"
|
||||
if !node.Online {
|
||||
status = "disabled"
|
||||
}
|
||||
return normalizeSubagentProfile(SubagentProfile{
|
||||
AgentID: agentID,
|
||||
Name: name + " Main Agent",
|
||||
Transport: "node",
|
||||
NodeID: strings.TrimSpace(node.ID),
|
||||
ParentAgentID: parentAgentID,
|
||||
Role: "remote_main",
|
||||
MemoryNamespace: agentID,
|
||||
Status: status,
|
||||
ManagedBy: "node_registry",
|
||||
})
|
||||
}
|
||||
|
||||
func nodeBranchAgentID(nodeID string) string {
|
||||
id := normalizeSubagentIdentifier(nodeID)
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
return "node." + id + ".main"
|
||||
}
|
||||
|
||||
type SubagentProfileTool struct {
|
||||
store *SubagentProfileStore
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/nodes"
|
||||
"clawgo/pkg/runtimecfg"
|
||||
)
|
||||
|
||||
@@ -185,3 +186,51 @@ func TestSubagentProfileStoreRejectsWritesForConfigManagedProfiles(t *testing.T)
|
||||
t.Fatalf("expected config-managed delete to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubagentProfileStoreIncludesNodeMainBranchProfiles(t *testing.T) {
|
||||
runtimecfg.Set(config.DefaultConfig())
|
||||
t.Cleanup(func() {
|
||||
runtimecfg.Set(config.DefaultConfig())
|
||||
nodes.DefaultManager().Remove("edge-dev")
|
||||
})
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Router.Enabled = true
|
||||
cfg.Agents.Router.MainAgentID = "main"
|
||||
cfg.Agents.Subagents["main"] = config.SubagentConfig{
|
||||
Enabled: true,
|
||||
Type: "router",
|
||||
SystemPromptFile: "agents/main/AGENT.md",
|
||||
}
|
||||
runtimecfg.Set(cfg)
|
||||
|
||||
nodes.DefaultManager().Upsert(nodes.NodeInfo{
|
||||
ID: "edge-dev",
|
||||
Name: "Edge Dev",
|
||||
Online: true,
|
||||
Capabilities: nodes.Capabilities{
|
||||
Model: true,
|
||||
},
|
||||
})
|
||||
|
||||
store := NewSubagentProfileStore(t.TempDir())
|
||||
profile, ok, err := store.Get(nodeBranchAgentID("edge-dev"))
|
||||
if err != nil {
|
||||
t.Fatalf("get failed: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("expected node-backed profile")
|
||||
}
|
||||
if profile.ManagedBy != "node_registry" || profile.Transport != "node" || profile.NodeID != "edge-dev" {
|
||||
t.Fatalf("unexpected node profile: %+v", profile)
|
||||
}
|
||||
if profile.ParentAgentID != "main" {
|
||||
t.Fatalf("expected main parent agent, got %+v", profile)
|
||||
}
|
||||
if _, err := store.Upsert(SubagentProfile{AgentID: profile.AgentID}); err == nil {
|
||||
t.Fatalf("expected node-managed upsert to fail")
|
||||
}
|
||||
if err := store.Delete(profile.AgentID); err == nil {
|
||||
t.Fatalf("expected node-managed delete to fail")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user