mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 06:47:30 +08:00
718 lines
20 KiB
Go
718 lines
20 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/YspCoder/clawgo/pkg/config"
|
|
"github.com/YspCoder/clawgo/pkg/nodes"
|
|
"github.com/YspCoder/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"`
|
|
NotifyMainPolicy string `json:"notify_main_policy,omitempty"`
|
|
Role string `json:"role,omitempty"`
|
|
SystemPromptFile string `json:"system_prompt_file,omitempty"`
|
|
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
|
|
MemoryNamespace string `json:"memory_namespace,omitempty"`
|
|
MaxRetries int `json:"max_retries,omitempty"`
|
|
RetryBackoff int `json:"retry_backoff_ms,omitempty"`
|
|
TimeoutSec int `json:"timeout_sec,omitempty"`
|
|
MaxTaskChars int `json:"max_task_chars,omitempty"`
|
|
MaxResultChars int `json:"max_result_chars,omitempty"`
|
|
Status string `json:"status"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
ManagedBy string `json:"managed_by,omitempty"`
|
|
}
|
|
|
|
type SubagentProfileStore struct {
|
|
workspace string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewSubagentProfileStore(workspace string) *SubagentProfileStore {
|
|
return &SubagentProfileStore{workspace: strings.TrimSpace(workspace)}
|
|
}
|
|
|
|
func (s *SubagentProfileStore) profilesDir() string {
|
|
return filepath.Join(s.workspace, "agents", "profiles")
|
|
}
|
|
|
|
func (s *SubagentProfileStore) profilePath(agentID string) string {
|
|
id := normalizeSubagentIdentifier(agentID)
|
|
return filepath.Join(s.profilesDir(), id+".json")
|
|
}
|
|
|
|
func (s *SubagentProfileStore) List() ([]SubagentProfile, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
merged, err := s.mergedProfilesLocked()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]SubagentProfile, 0, len(merged))
|
|
for _, p := range merged {
|
|
out = append(out, p)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].UpdatedAt != out[j].UpdatedAt {
|
|
return out[i].UpdatedAt > out[j].UpdatedAt
|
|
}
|
|
return out[i].AgentID < out[j].AgentID
|
|
})
|
|
return out, nil
|
|
}
|
|
|
|
func (s *SubagentProfileStore) Get(agentID string) (*SubagentProfile, bool, error) {
|
|
id := normalizeSubagentIdentifier(agentID)
|
|
if id == "" {
|
|
return nil, false, nil
|
|
}
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
merged, err := s.mergedProfilesLocked()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
p, ok := merged[id]
|
|
if !ok {
|
|
return nil, false, nil
|
|
}
|
|
cp := p
|
|
return &cp, true, nil
|
|
}
|
|
|
|
func (s *SubagentProfileStore) FindByRole(role string) (*SubagentProfile, bool, error) {
|
|
target := strings.ToLower(strings.TrimSpace(role))
|
|
if target == "" {
|
|
return nil, false, nil
|
|
}
|
|
items, err := s.List()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
for _, p := range items {
|
|
if strings.ToLower(strings.TrimSpace(p.Role)) == target {
|
|
cp := p
|
|
return &cp, true, nil
|
|
}
|
|
}
|
|
return nil, false, nil
|
|
}
|
|
|
|
func (s *SubagentProfileStore) Upsert(profile SubagentProfile) (*SubagentProfile, error) {
|
|
p := normalizeSubagentProfile(profile)
|
|
if p.AgentID == "" {
|
|
return nil, fmt.Errorf("agent_id is required")
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
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)
|
|
existing := SubagentProfile{}
|
|
if b, err := os.ReadFile(path); err == nil {
|
|
_ = json.Unmarshal(b, &existing)
|
|
}
|
|
existing = normalizeSubagentProfile(existing)
|
|
if existing.CreatedAt > 0 {
|
|
p.CreatedAt = existing.CreatedAt
|
|
} else if p.CreatedAt <= 0 {
|
|
p.CreatedAt = now
|
|
}
|
|
p.UpdatedAt = now
|
|
|
|
if err := os.MkdirAll(s.profilesDir(), 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := json.MarshalIndent(p, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.WriteFile(path, b, 0644); err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
func (s *SubagentProfileStore) Delete(agentID string) error {
|
|
id := normalizeSubagentIdentifier(agentID)
|
|
if id == "" {
|
|
return fmt.Errorf("agent_id is required")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
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) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeSubagentProfile(in SubagentProfile) SubagentProfile {
|
|
p := in
|
|
p.AgentID = normalizeSubagentIdentifier(p.AgentID)
|
|
p.Name = strings.TrimSpace(p.Name)
|
|
if p.Name == "" {
|
|
p.Name = p.AgentID
|
|
}
|
|
p.Transport = normalizeProfileTransport(p.Transport)
|
|
p.NodeID = strings.TrimSpace(p.NodeID)
|
|
p.ParentAgentID = normalizeSubagentIdentifier(p.ParentAgentID)
|
|
p.NotifyMainPolicy = normalizeNotifyMainPolicy(p.NotifyMainPolicy)
|
|
p.Role = strings.TrimSpace(p.Role)
|
|
p.SystemPromptFile = strings.TrimSpace(p.SystemPromptFile)
|
|
p.MemoryNamespace = normalizeSubagentIdentifier(p.MemoryNamespace)
|
|
if p.MemoryNamespace == "" {
|
|
p.MemoryNamespace = p.AgentID
|
|
}
|
|
p.Status = normalizeProfileStatus(p.Status)
|
|
p.ToolAllowlist = normalizeToolAllowlist(p.ToolAllowlist)
|
|
p.ManagedBy = strings.TrimSpace(p.ManagedBy)
|
|
p.MaxRetries = clampInt(p.MaxRetries, 0, 8)
|
|
p.RetryBackoff = clampInt(p.RetryBackoff, 500, 120000)
|
|
p.TimeoutSec = clampInt(p.TimeoutSec, 0, 3600)
|
|
p.MaxTaskChars = clampInt(p.MaxTaskChars, 0, 400000)
|
|
p.MaxResultChars = clampInt(p.MaxResultChars, 0, 400000)
|
|
return p
|
|
}
|
|
|
|
func normalizeProfileStatus(s string) string {
|
|
v := strings.ToLower(strings.TrimSpace(s))
|
|
switch v {
|
|
case "active", "disabled":
|
|
return v
|
|
default:
|
|
return "active"
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
seen := make(map[string]struct{}, len(in))
|
|
out := make([]string, 0, len(in))
|
|
for _, item := range in {
|
|
v := strings.TrimSpace(item)
|
|
if v == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[v]; ok {
|
|
continue
|
|
}
|
|
seen[v] = struct{}{}
|
|
out = append(out, v)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func normalizeToolAllowlist(in []string) []string {
|
|
items := ExpandToolAllowlistEntries(normalizeStringList(in))
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
items = normalizeStringList(items)
|
|
sort.Strings(items)
|
|
return items
|
|
}
|
|
|
|
func clampInt(v, min, max int) int {
|
|
if v < min {
|
|
return min
|
|
}
|
|
if max > 0 && v > max {
|
|
return max
|
|
}
|
|
return v
|
|
}
|
|
|
|
func parseStringList(raw interface{}) []string {
|
|
return normalizeStringList(MapStringListArg(map[string]interface{}{"items": raw}, "items"))
|
|
}
|
|
|
|
func (s *SubagentProfileStore) mergedProfilesLocked() (map[string]SubagentProfile, error) {
|
|
merged := make(map[string]SubagentProfile)
|
|
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
|
|
}
|
|
for _, p := range fileProfiles {
|
|
if _, exists := merged[p.AgentID]; exists {
|
|
continue
|
|
}
|
|
merged[p.AgentID] = p
|
|
}
|
|
return merged, nil
|
|
}
|
|
|
|
func (s *SubagentProfileStore) fileProfilesLocked() ([]SubagentProfile, error) {
|
|
dir := s.profilesDir()
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return []SubagentProfile{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
out := make([]SubagentProfile, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".json") {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, e.Name())
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var p SubagentProfile
|
|
if err := json.Unmarshal(b, &p); err != nil {
|
|
continue
|
|
}
|
|
out = append(out, normalizeSubagentProfile(p))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *SubagentProfileStore) configProfilesLocked() []SubagentProfile {
|
|
cfg := runtimecfg.Get()
|
|
if cfg == nil || len(cfg.Agents.Subagents) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]SubagentProfile, 0, len(cfg.Agents.Subagents))
|
|
for agentID, subcfg := range cfg.Agents.Subagents {
|
|
profile := profileFromConfig(agentID, subcfg)
|
|
if profile.AgentID == "" {
|
|
continue
|
|
}
|
|
out = append(out, profile)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *SubagentProfileStore) configProfileLocked(agentID string) (SubagentProfile, bool) {
|
|
id := normalizeSubagentIdentifier(agentID)
|
|
if id == "" {
|
|
return SubagentProfile{}, false
|
|
}
|
|
cfg := runtimecfg.Get()
|
|
if cfg == nil {
|
|
return SubagentProfile{}, false
|
|
}
|
|
subcfg, ok := cfg.Agents.Subagents[id]
|
|
if !ok {
|
|
return SubagentProfile{}, false
|
|
}
|
|
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() {
|
|
if isLocalNode(node.ID) {
|
|
continue
|
|
}
|
|
for _, profile := range profilesFromNode(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 {
|
|
status = "disabled"
|
|
}
|
|
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),
|
|
NotifyMainPolicy: strings.TrimSpace(subcfg.NotifyMainPolicy),
|
|
Role: strings.TrimSpace(subcfg.Role),
|
|
SystemPromptFile: strings.TrimSpace(subcfg.SystemPromptFile),
|
|
ToolAllowlist: append([]string(nil), subcfg.Tools.Allowlist...),
|
|
MemoryNamespace: strings.TrimSpace(subcfg.MemoryNamespace),
|
|
MaxRetries: subcfg.Runtime.MaxRetries,
|
|
RetryBackoff: subcfg.Runtime.RetryBackoffMs,
|
|
TimeoutSec: subcfg.Runtime.TimeoutSec,
|
|
MaxTaskChars: subcfg.Runtime.MaxTaskChars,
|
|
MaxResultChars: subcfg.Runtime.MaxResultChars,
|
|
Status: status,
|
|
ManagedBy: "config.json",
|
|
})
|
|
}
|
|
|
|
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 {
|
|
if isLocalNode(node.ID) {
|
|
continue
|
|
}
|
|
profiles := profilesFromNode(node, parentAgentID)
|
|
for _, profile := range profiles {
|
|
if profile.AgentID == "" {
|
|
continue
|
|
}
|
|
out = append(out, profile)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func profilesFromNode(node nodes.NodeInfo, parentAgentID string) []SubagentProfile {
|
|
name := strings.TrimSpace(node.Name)
|
|
if name == "" {
|
|
name = strings.TrimSpace(node.ID)
|
|
}
|
|
status := "active"
|
|
if !node.Online {
|
|
status = "disabled"
|
|
}
|
|
rootAgentID := nodeBranchAgentID(node.ID)
|
|
if rootAgentID == "" {
|
|
return nil
|
|
}
|
|
out := []SubagentProfile{normalizeSubagentProfile(SubagentProfile{
|
|
AgentID: rootAgentID,
|
|
Name: name + " Main Agent",
|
|
Transport: "node",
|
|
NodeID: strings.TrimSpace(node.ID),
|
|
ParentAgentID: parentAgentID,
|
|
Role: "remote_main",
|
|
MemoryNamespace: rootAgentID,
|
|
Status: status,
|
|
ManagedBy: "node_registry",
|
|
})}
|
|
for _, agent := range node.Agents {
|
|
agentID := normalizeSubagentIdentifier(agent.ID)
|
|
if agentID == "" || agentID == "main" {
|
|
continue
|
|
}
|
|
out = append(out, normalizeSubagentProfile(SubagentProfile{
|
|
AgentID: nodeChildAgentID(node.ID, agentID),
|
|
Name: nodeChildAgentDisplayName(name, agent),
|
|
Transport: "node",
|
|
NodeID: strings.TrimSpace(node.ID),
|
|
ParentAgentID: rootAgentID,
|
|
Role: strings.TrimSpace(agent.Role),
|
|
MemoryNamespace: nodeChildAgentID(node.ID, agentID),
|
|
Status: status,
|
|
ManagedBy: "node_registry",
|
|
}))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func nodeBranchAgentID(nodeID string) string {
|
|
id := normalizeSubagentIdentifier(nodeID)
|
|
if id == "" {
|
|
return ""
|
|
}
|
|
return "node." + id + ".main"
|
|
}
|
|
|
|
func nodeChildAgentID(nodeID, agentID string) string {
|
|
nodeID = normalizeSubagentIdentifier(nodeID)
|
|
agentID = normalizeSubagentIdentifier(agentID)
|
|
if nodeID == "" || agentID == "" {
|
|
return ""
|
|
}
|
|
return "node." + nodeID + "." + agentID
|
|
}
|
|
|
|
func nodeChildAgentDisplayName(nodeName string, agent nodes.AgentInfo) string {
|
|
base := strings.TrimSpace(agent.DisplayName)
|
|
if base == "" {
|
|
base = strings.TrimSpace(agent.ID)
|
|
}
|
|
nodeName = strings.TrimSpace(nodeName)
|
|
if nodeName == "" {
|
|
return base
|
|
}
|
|
return nodeName + " / " + base
|
|
}
|
|
|
|
func isLocalNode(nodeID string) bool {
|
|
return normalizeSubagentIdentifier(nodeID) == "local"
|
|
}
|
|
|
|
type SubagentProfileTool struct {
|
|
store *SubagentProfileStore
|
|
}
|
|
|
|
func NewSubagentProfileTool(store *SubagentProfileStore) *SubagentProfileTool {
|
|
return &SubagentProfileTool{store: store}
|
|
}
|
|
|
|
func (t *SubagentProfileTool) Name() string { return "subagent_profile" }
|
|
|
|
func (t *SubagentProfileTool) Description() string {
|
|
return "Manage subagent profiles: create/list/get/update/enable/disable/delete."
|
|
}
|
|
|
|
func (t *SubagentProfileTool) Parameters() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"action": map[string]interface{}{"type": "string", "description": "create|list|get|update|enable|disable|delete"},
|
|
"agent_id": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Unique subagent id, e.g. coder/writer/tester",
|
|
},
|
|
"name": map[string]interface{}{"type": "string"},
|
|
"notify_main_policy": map[string]interface{}{"type": "string", "description": "final_only|internal_only|milestone|on_blocked|always"},
|
|
"role": map[string]interface{}{"type": "string"},
|
|
"system_prompt_file": map[string]interface{}{"type": "string"},
|
|
"memory_namespace": map[string]interface{}{"type": "string"},
|
|
"status": map[string]interface{}{"type": "string", "description": "active|disabled"},
|
|
"tool_allowlist": map[string]interface{}{
|
|
"type": "array",
|
|
"description": "Tool allowlist entries. Supports tool names, '*'/'all', and grouped tokens like 'group:files_read'.",
|
|
"items": map[string]interface{}{"type": "string"},
|
|
},
|
|
"max_retries": map[string]interface{}{"type": "integer", "description": "Retry limit for subagent task execution."},
|
|
"retry_backoff_ms": map[string]interface{}{"type": "integer", "description": "Backoff between retries in milliseconds."},
|
|
"timeout_sec": map[string]interface{}{"type": "integer", "description": "Per-attempt timeout in seconds."},
|
|
"max_task_chars": map[string]interface{}{"type": "integer", "description": "Task input size quota (characters)."},
|
|
"max_result_chars": map[string]interface{}{"type": "integer", "description": "Result output size quota (characters)."},
|
|
},
|
|
"required": []string{"action"},
|
|
}
|
|
}
|
|
|
|
func (t *SubagentProfileTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
|
|
_ = ctx
|
|
if t.store == nil {
|
|
return "subagent profile store not available", nil
|
|
}
|
|
action := strings.ToLower(MapStringArg(args, "action"))
|
|
agentID := normalizeSubagentIdentifier(MapStringArg(args, "agent_id"))
|
|
|
|
switch action {
|
|
case "list":
|
|
items, err := t.store.List()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(items) == 0 {
|
|
return "No subagent profiles.", nil
|
|
}
|
|
var sb strings.Builder
|
|
sb.WriteString("Subagent Profiles:\n")
|
|
for i, p := range items {
|
|
sb.WriteString(fmt.Sprintf("- #%d %s [%s] role=%s memory_ns=%s\n", i+1, p.AgentID, p.Status, p.Role, p.MemoryNamespace))
|
|
}
|
|
return strings.TrimSpace(sb.String()), nil
|
|
case "get":
|
|
if agentID == "" {
|
|
return "agent_id is required", nil
|
|
}
|
|
p, ok, err := t.store.Get(agentID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !ok {
|
|
return "subagent profile not found", nil
|
|
}
|
|
b, _ := json.MarshalIndent(p, "", " ")
|
|
return string(b), nil
|
|
case "create":
|
|
if agentID == "" {
|
|
return "agent_id is required", nil
|
|
}
|
|
if _, ok, err := t.store.Get(agentID); err != nil {
|
|
return "", err
|
|
} else if ok {
|
|
return "subagent profile already exists", nil
|
|
}
|
|
p := SubagentProfile{
|
|
AgentID: agentID,
|
|
Name: stringArg(args, "name"),
|
|
NotifyMainPolicy: stringArg(args, "notify_main_policy"),
|
|
Role: stringArg(args, "role"),
|
|
SystemPromptFile: stringArg(args, "system_prompt_file"),
|
|
MemoryNamespace: stringArg(args, "memory_namespace"),
|
|
Status: stringArg(args, "status"),
|
|
ToolAllowlist: parseStringList(args["tool_allowlist"]),
|
|
MaxRetries: profileIntArg(args, "max_retries"),
|
|
RetryBackoff: profileIntArg(args, "retry_backoff_ms"),
|
|
TimeoutSec: profileIntArg(args, "timeout_sec"),
|
|
MaxTaskChars: profileIntArg(args, "max_task_chars"),
|
|
MaxResultChars: profileIntArg(args, "max_result_chars"),
|
|
}
|
|
saved, err := t.store.Upsert(p)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Created subagent profile: %s (role=%s status=%s)", saved.AgentID, saved.Role, saved.Status), nil
|
|
case "update":
|
|
if agentID == "" {
|
|
return "agent_id is required", nil
|
|
}
|
|
existing, ok, err := t.store.Get(agentID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !ok {
|
|
return "subagent profile not found", nil
|
|
}
|
|
next := *existing
|
|
if _, ok := args["name"]; ok {
|
|
next.Name = stringArg(args, "name")
|
|
}
|
|
if _, ok := args["role"]; ok {
|
|
next.Role = stringArg(args, "role")
|
|
}
|
|
if _, ok := args["notify_main_policy"]; ok {
|
|
next.NotifyMainPolicy = stringArg(args, "notify_main_policy")
|
|
}
|
|
if _, ok := args["system_prompt_file"]; ok {
|
|
next.SystemPromptFile = stringArg(args, "system_prompt_file")
|
|
}
|
|
if _, ok := args["memory_namespace"]; ok {
|
|
next.MemoryNamespace = stringArg(args, "memory_namespace")
|
|
}
|
|
if _, ok := args["status"]; ok {
|
|
next.Status = stringArg(args, "status")
|
|
}
|
|
if _, ok := args["tool_allowlist"]; ok {
|
|
next.ToolAllowlist = parseStringList(args["tool_allowlist"])
|
|
}
|
|
if _, ok := args["max_retries"]; ok {
|
|
next.MaxRetries = profileIntArg(args, "max_retries")
|
|
}
|
|
if _, ok := args["retry_backoff_ms"]; ok {
|
|
next.RetryBackoff = profileIntArg(args, "retry_backoff_ms")
|
|
}
|
|
if _, ok := args["timeout_sec"]; ok {
|
|
next.TimeoutSec = profileIntArg(args, "timeout_sec")
|
|
}
|
|
if _, ok := args["max_task_chars"]; ok {
|
|
next.MaxTaskChars = profileIntArg(args, "max_task_chars")
|
|
}
|
|
if _, ok := args["max_result_chars"]; ok {
|
|
next.MaxResultChars = profileIntArg(args, "max_result_chars")
|
|
}
|
|
saved, err := t.store.Upsert(next)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Updated subagent profile: %s (role=%s status=%s)", saved.AgentID, saved.Role, saved.Status), nil
|
|
case "enable", "disable":
|
|
if agentID == "" {
|
|
return "agent_id is required", nil
|
|
}
|
|
existing, ok, err := t.store.Get(agentID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !ok {
|
|
return "subagent profile not found", nil
|
|
}
|
|
if action == "enable" {
|
|
existing.Status = "active"
|
|
} else {
|
|
existing.Status = "disabled"
|
|
}
|
|
saved, err := t.store.Upsert(*existing)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Subagent profile %s set to %s", saved.AgentID, saved.Status), nil
|
|
case "delete":
|
|
if agentID == "" {
|
|
return "agent_id is required", nil
|
|
}
|
|
if err := t.store.Delete(agentID); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted subagent profile: %s", agentID), nil
|
|
default:
|
|
return "unsupported action", nil
|
|
}
|
|
}
|
|
|
|
func stringArg(args map[string]interface{}, key string) string {
|
|
return MapStringArg(args, key)
|
|
}
|
|
|
|
func profileIntArg(args map[string]interface{}, key string) int {
|
|
return MapIntArg(args, key, 0)
|
|
}
|