package methods import ( "context" "encoding/json" "fmt" "log/slog" "os" "path/filepath" "github.com/nextlevelbuilder/goclaw/internal/bootstrap" "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.create --- // Matching TS src/gateway/server-methods/agents.ts:216-287 func (m *AgentsMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { locale := store.LocaleFromContext(ctx) var params struct { Name string `json:"name"` Workspace string `json:"workspace"` Emoji string `json:"emoji"` Avatar string `json:"avatar"` AgentType string `json:"agent_type"` // "open" (default) or "predefined" OwnerIDs []string `json:"owner_ids,omitempty"` // first entry used as DB owner_id; falls back to "system" // 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.Name == "" { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name"))) return } agentType := params.AgentType if agentType == "" { agentType = store.AgentTypeOpen } agentID := config.NormalizeAgentID(params.Name) if agentID == "default" { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "cannot create agent with reserved id 'default'"))) return } // Resolve workspace ws := params.Workspace if ws == "" { ws = filepath.Join(m.workspace, "agents", agentID) } else { ws = config.ExpandHome(ws) } if m.agentStore != nil { // --- DB-backed: create agent in store --- ctx := context.Background() // Check if agent already exists in DB if existing, _ := m.agentStore.GetByKey(ctx, agentID); existing != nil { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAlreadyExists, "agent", agentID))) return } // Resolve owner: use first provided ID so external provisioning tools (e.g. goclaw-wizards) // can set a real user as owner at creation time. Falls back to "system" for backward compat. ownerID := "system" if len(params.OwnerIDs) > 0 && params.OwnerIDs[0] != "" { ownerID = params.OwnerIDs[0] } agentData := &store.AgentData{ AgentKey: agentID, DisplayName: params.Name, OwnerID: ownerID, AgentType: agentType, Provider: m.cfg.Agents.Defaults.Provider, Model: m.cfg.Agents.Defaults.Model, Workspace: ws, Status: store.AgentStatusActive, ToolsConfig: params.ToolsConfig, SubagentsConfig: params.SubagentsConfig, SandboxConfig: params.SandboxConfig, MemoryConfig: params.MemoryConfig, CompactionConfig: params.CompactionConfig, ContextPruning: params.ContextPruning, OtherConfig: params.OtherConfig, } if err := m.agentStore.Create(ctx, agentData); err != nil { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "agent", fmt.Sprintf("%v", err)))) return } // Seed context files to DB (skipped for open agents) if _, err := bootstrap.SeedToStore(ctx, m.agentStore, agentData.ID, agentData.AgentType); err != nil { slog.Warn("failed to seed bootstrap for agent", "agent", agentID, "error", err) } // Set identity in DB bootstrap if params.Name != "" || params.Emoji != "" || params.Avatar != "" { content := buildIdentityContent(params.Name, params.Emoji, params.Avatar) if err := m.agentStore.SetAgentContextFile(ctx, agentData.ID, "IDENTITY.md", content); err != nil { slog.Warn("failed to set IDENTITY.md", "agent", agentID, "error", err) } } // Invalidate router cache so resolver re-loads from DB m.agents.InvalidateAgent(agentID) } else { // --- Fallback: config.json + filesystem --- if _, ok := m.cfg.Agents.List[agentID]; ok { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAlreadyExists, "agent", agentID))) return } spec := config.AgentSpec{ DisplayName: params.Name, Workspace: ws, } if params.Emoji != "" || params.Avatar != "" { spec.Identity = &config.IdentityConfig{ Emoji: params.Emoji, } } if m.cfg.Agents.List == nil { m.cfg.Agents.List = make(map[string]config.AgentSpec) } m.cfg.Agents.List[agentID] = spec 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 } // Append identity metadata to IDENTITY.md if params.Name != "" || params.Emoji != "" || params.Avatar != "" { identityPath := filepath.Join(ws, "IDENTITY.md") appendIdentityFields(identityPath, params.Name, params.Emoji, params.Avatar) } } // Both modes: create workspace dir + seed filesystem backup os.MkdirAll(ws, 0755) bootstrap.EnsureWorkspaceFiles(ws) client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "ok": true, "agentId": agentID, "name": params.Name, "workspace": ws, })) emitAudit(m.eventBus, client, "agent.created", "agent", agentID) }