package bootstrap import ( "context" "log/slog" "path/filepath" "github.com/google/uuid" "github.com/nextlevelbuilder/goclaw/internal/store" ) // SeedToStore seeds embedded templates into agent_context_files (agent-level). // Used for predefined agents only — open agents get per-user files via SeedUserFiles. // Only writes files that don't already have content. // Returns the list of file names that were seeded. func SeedToStore(ctx context.Context, agentStore store.AgentStore, agentID uuid.UUID, agentType string) ([]string, error) { // Open agents don't need agent-level context files — // all files are seeded per-user from embedded templates on first chat. if agentType == store.AgentTypeOpen { return nil, nil } existing, err := agentStore.GetAgentContextFiles(ctx, agentID) if err != nil { return nil, err } // Build set of files that already have content hasContent := make(map[string]bool) for _, f := range existing { if f.Content != "" { hasContent[f.FileName] = true } } var seeded []string for _, name := range templateFiles { // USER.md is always per-user for predefined agents — don't seed at agent level. // Having it at agent level alongside the user copy can confuse the model. if name == UserFile { continue } // TOOLS.md: local tool notes (camera, SSH, device names) — not applicable. if name == ToolsFile { continue } if hasContent[name] { continue } content, err := templateFS.ReadFile(filepath.Join("templates", name)) if err != nil { slog.Warn("bootstrap: failed to read embedded template", "file", name, "error", err) continue } if err := agentStore.SetAgentContextFile(ctx, agentID, name, string(content)); err != nil { return seeded, err } seeded = append(seeded, name) } // Seed USER_PREDEFINED.md for predefined agents (agent-level, not in templateFiles). // Provides baseline user-handling rules shared across all users. if !hasContent[UserPredefinedFile] { content, err := templateFS.ReadFile(filepath.Join("templates", UserPredefinedFile)) if err == nil { if err := agentStore.SetAgentContextFile(ctx, agentID, UserPredefinedFile, string(content)); err != nil { return seeded, err } seeded = append(seeded, UserPredefinedFile) } } if len(seeded) > 0 { slog.Info("seeded agent context files to store", "agent", agentID, "files", seeded) } return seeded, nil } // userSeedFilesOpen is the full set of files seeded per-user for open agents. // TOOLS.md excluded — not applicable. var userSeedFilesOpen = []string{ AgentsFile, SoulFile, IdentityFile, UserFile, BootstrapFile, } // userSeedFilesPredefined is the set of files seeded per-user for predefined agents. // Only USER.md — predefined agents already have full context (SOUL.md, IDENTITY.md, AGENTS.md) // and don't need a bootstrap onboarding ritual. They just need to learn the user's profile. var userSeedFilesPredefined = []string{ UserFile, BootstrapFile, } // SeedUserFiles seeds embedded templates into user_context_files for a new user. // For "open" agents: all 7 files (including BOOTSTRAP.md). // For "predefined" agents: USER.md + BOOTSTRAP.md (user-focused onboarding template). // Only writes files that don't already exist — safe to call multiple times. // // For predefined agents seeding USER.md: if the agent already has a populated // USER.md in agent_context_files (e.g. written by the wizard or management dashboard), // that content is used as the per-user seed instead of the blank embedded template. // This ensures wizard-configured owner profiles are preserved on first chat. // // Returns the list of file names that were seeded. func SeedUserFiles(ctx context.Context, agentStore store.AgentStore, agentID uuid.UUID, userID, agentType string) ([]string, error) { files := userSeedFilesOpen if agentType == store.AgentTypePredefined { files = userSeedFilesPredefined } // Check existing per-user files to avoid overwriting personalized content existing, err := agentStore.GetUserContextFiles(ctx, agentID, userID) if err != nil { return nil, err } hasFile := make(map[string]bool, len(existing)) for _, f := range existing { if f.Content != "" { hasFile[f.FileName] = true } } // For predefined agents: load agent-level files once to use as seed fallback. // USER.md at agent-level may contain a pre-configured owner profile (e.g. set by // the wizard or management dashboard). Use it as the per-user seed instead of the // blank embedded template so the agent starts with the correct owner context. var agentLevelFiles map[string]string if agentType == store.AgentTypePredefined { agentFiles, err := agentStore.GetAgentContextFiles(ctx, agentID) if err == nil && len(agentFiles) > 0 { agentLevelFiles = make(map[string]string, len(agentFiles)) for _, f := range agentFiles { if f.Content != "" { agentLevelFiles[f.FileName] = f.Content } } } } var seeded []string for _, name := range files { if hasFile[name] { continue // already has personalized content, don't overwrite } // For predefined agents seeding USER.md: prefer agent-level content as seed. // This propagates wizard/dashboard-configured owner profile to the first user. if agentType == store.AgentTypePredefined && name == UserFile { if agentContent, ok := agentLevelFiles[name]; ok { if err := agentStore.SetUserContextFile(ctx, agentID, userID, name, agentContent); err != nil { return seeded, err } seeded = append(seeded, name) continue } // No agent-level USER.md → fall through to blank embedded template } // Predefined agents use a user-focused bootstrap template templateName := name if agentType == store.AgentTypePredefined && name == BootstrapFile { templateName = "BOOTSTRAP_PREDEFINED.md" } content, err := templateFS.ReadFile(filepath.Join("templates", templateName)) if err != nil { slog.Warn("bootstrap: failed to read embedded template for user seed", "file", name, "error", err) continue } if err := agentStore.SetUserContextFile(ctx, agentID, userID, name, string(content)); err != nil { return seeded, err } seeded = append(seeded, name) } if len(seeded) > 0 { slog.Info("seeded user context files", "agent", agentID, "user", userID, "type", agentType, "files", seeded) } return seeded, nil }