package methods import ( "context" "encoding/json" "fmt" "log/slog" "os" "path/filepath" "github.com/nextlevelbuilder/goclaw/internal/config" "github.com/nextlevelbuilder/goclaw/internal/gateway" "github.com/nextlevelbuilder/goclaw/internal/i18n" "github.com/nextlevelbuilder/goclaw/internal/store" "github.com/nextlevelbuilder/goclaw/pkg/protocol" ) // --- agents.update --- // Matching TS src/gateway/server-methods/agents.ts:288-346 func (m *AgentsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { locale := store.LocaleFromContext(ctx) var params struct { AgentID string `json:"agentId"` Name string `json:"name"` Workspace string `json:"workspace"` Model string `json:"model"` Avatar string `json:"avatar"` // Per-agent config overrides ToolsConfig json.RawMessage `json:"tools_config,omitempty"` SubagentsConfig json.RawMessage `json:"subagents_config,omitempty"` SandboxConfig json.RawMessage `json:"sandbox_config,omitempty"` MemoryConfig json.RawMessage `json:"memory_config,omitempty"` CompactionConfig json.RawMessage `json:"compaction_config,omitempty"` ContextPruning json.RawMessage `json:"context_pruning,omitempty"` OtherConfig json.RawMessage `json:"other_config,omitempty"` } if req.Params != nil { json.Unmarshal(req.Params, ¶ms) } if params.AgentID == "" { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "agentId"))) return } if m.agentStore != nil { // --- DB-backed: update agent in store --- ctx := context.Background() ag, err := m.agentStore.GetByKey(ctx, params.AgentID) if err != nil { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID))) return } updates := map[string]any{} if params.Name != "" { updates["display_name"] = params.Name } if params.Workspace != "" { ws := config.ExpandHome(params.Workspace) updates["workspace"] = ws os.MkdirAll(ws, 0755) } if params.Model != "" { updates["model"] = params.Model } // Per-agent JSONB config overrides if len(params.ToolsConfig) > 0 { updates["tools_config"] = []byte(params.ToolsConfig) } if len(params.SubagentsConfig) > 0 { updates["subagents_config"] = []byte(params.SubagentsConfig) } if len(params.SandboxConfig) > 0 { updates["sandbox_config"] = []byte(params.SandboxConfig) } if len(params.MemoryConfig) > 0 { updates["memory_config"] = []byte(params.MemoryConfig) } if len(params.CompactionConfig) > 0 { updates["compaction_config"] = []byte(params.CompactionConfig) } if len(params.ContextPruning) > 0 { updates["context_pruning"] = []byte(params.ContextPruning) } if len(params.OtherConfig) > 0 { updates["other_config"] = []byte(params.OtherConfig) } if len(updates) > 0 { if err := m.agentStore.Update(ctx, ag.ID, updates); err != nil { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToUpdate, "agent", fmt.Sprintf("%v", err)))) return } } // Update identity in DB bootstrap — preserve existing fields not being changed. if params.Avatar != "" || params.Name != "" { // Read existing identity to preserve emoji and other fields. existingEmoji, existingAvatar, existingName := "", "", "" if dbFiles, err := m.agentStore.GetAgentContextFiles(ctx, ag.ID); err == nil { for _, f := range dbFiles { if f.FileName == "IDENTITY.md" { if identity := parseIdentityContent(f.Content); identity != nil { existingEmoji = identity["Emoji"] existingAvatar = identity["Avatar"] existingName = identity["Name"] } break } } } name := params.Name if name == "" { name = existingName } avatar := params.Avatar if avatar == "" { avatar = existingAvatar } content := buildIdentityContent(name, existingEmoji, avatar) if err := m.agentStore.SetAgentContextFile(ctx, ag.ID, "IDENTITY.md", content); err != nil { slog.Warn("failed to update IDENTITY.md", "agent", params.AgentID, "error", err) } // Invalidate interceptor cache so updated IDENTITY.md is served immediately if m.interceptor != nil { m.interceptor.InvalidateAgent(ag.ID) } } m.agents.InvalidateAgent(params.AgentID) } else { // --- Fallback: config.json --- spec, ok := m.cfg.Agents.List[params.AgentID] if !ok { if params.AgentID != "default" { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID))) return } } if params.Name != "" { spec.DisplayName = params.Name } if params.Workspace != "" { spec.Workspace = config.ExpandHome(params.Workspace) os.MkdirAll(spec.Workspace, 0755) } if params.Model != "" { spec.Model = params.Model } if params.AgentID == "default" { if params.Model != "" { m.cfg.Agents.Defaults.Model = params.Model } if params.Workspace != "" { m.cfg.Agents.Defaults.Workspace = params.Workspace } } else { m.cfg.Agents.List[params.AgentID] = spec } if params.Avatar != "" { ws := spec.Workspace if ws == "" { ws = config.ExpandHome(m.cfg.Agents.Defaults.Workspace) } identityPath := filepath.Join(ws, "IDENTITY.md") appendIdentityFields(identityPath, "", "", params.Avatar) } if err := config.Save(m.cfgPath, m.cfg); err != nil { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error()))) return } } client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "agentId": params.AgentID, })) emitAudit(m.eventBus, client, "agent.updated", "agent", params.AgentID) }