feat(bootstrap): skip built-in USER.md for predefined agents with USER_PREDEFINED.md (#1397)

When a predefined agent has an operator-authored USER_PREDEFINED.md, the
built-in per-user USER.md template is no longer seeded or injected into the
system prompt. This gives operators full control over user-context in the
prompt and stops the per-turn name/timezone profile nag. Open agents and
predefined agents without USER_PREDEFINED.md are unaffected.

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
This commit is contained in:
Bruno ClermontandBruno Clermont authored and GitHub committed 2026-07-09 03:24:15 +07:00
1 parent 6bafe54b08
commit d3c04ab3e5
4 files changed
+169 -22

No files matched your search

+30 -18
View File
@@ -196,10 +196,35 @@ func SeedUserFiles(ctx context.Context, agentStore store.AgentStore, agentID uui
return nil, nil
}
// 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
}
}
}
}
// When a predefined agent has an operator-authored USER_PREDEFINED.md, the
// operator owns the entire user-context portion of the system prompt. Skip
// seeding the built-in USER.md and BOOTSTRAP.md templates so they never
// impose themselves (or their per-turn name/timezone/pronoun nag) on top
// of the operator's own content.
_, hasUserPredefined := agentLevelFiles[UserPredefinedFile]
skipBuiltinUserFiles := agentType == store.AgentTypePredefined && hasUserPredefined
// Channel-provided contact info: skip bootstrap, pre-fill USER.md directly.
// Currently only Pancake channel (Facebook Messenger) provides enough user info
// (display_name from Facebook profile) to skip the interactive onboarding flow.
if shouldSkipBootstrap(channelMeta) {
if !skipBuiltinUserFiles && shouldSkipBootstrap(channelMeta) {
userContent := buildPrefilledUser(channelMeta)
if err := retryOnBusy(func() error {
return agentStore.SetUserContextFile(ctx, agentID, userID, UserFile, userContent)
@@ -241,29 +266,16 @@ func SeedUserFiles(ctx context.Context, agentStore store.AgentStore, agentID uui
}
}
// 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
}
if skipBuiltinUserFiles && (name == UserFile || name == BootstrapFile) {
continue // operator-authored USER_PREDEFINED.md owns the user-context prompt
}
// 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 {
+51
View File
@@ -235,6 +235,57 @@ func TestSeedUserFiles_PredefinedAgent_DoesNotOverwriteExistingPerUserContent(t
}
}
// TestSeedUserFiles_PredefinedAgent_WithUserPredefined_SkipsBuiltinUserAndBootstrap
// verifies that when a predefined agent has an operator-authored USER_PREDEFINED.md,
// SeedUserFiles skips seeding both the built-in USER.md and BOOTSTRAP.md templates —
// the operator owns the user-context portion of the system prompt.
func TestSeedUserFiles_PredefinedAgent_WithUserPredefined_SkipsBuiltinUserAndBootstrap(t *testing.T) {
as := newSeedStub()
agentID := uuid.New()
as.agentFiles[UserPredefinedFile] = "# USER_PREDEFINED.md\nOperator-authored user context"
seeded, err := SeedUserFiles(context.Background(), as, agentID, "user-erin", store.AgentTypePredefined, false, nil)
if err != nil {
t.Fatalf("SeedUserFiles returned error: %v", err)
}
for _, f := range seeded {
if f == UserFile || f == BootstrapFile {
t.Errorf("USER.md/BOOTSTRAP.md must not be seeded when USER_PREDEFINED.md exists, got seeded=%v", seeded)
}
}
if _, wrote := as.seededUserFiles[UserFile]; wrote {
t.Error("USER.md should not be written when USER_PREDEFINED.md exists")
}
if _, wrote := as.seededUserFiles[BootstrapFile]; wrote {
t.Error("BOOTSTRAP.md should not be written when USER_PREDEFINED.md exists")
}
}
// TestSeedUserFiles_PredefinedAgent_WithUserPredefined_SkipsChannelPrefill verifies
// that the channel-provided contact info prefill path (shouldSkipBootstrap) also
// respects USER_PREDEFINED.md and does not write a prefilled USER.md.
func TestSeedUserFiles_PredefinedAgent_WithUserPredefined_SkipsChannelPrefill(t *testing.T) {
as := newSeedStub()
agentID := uuid.New()
as.agentFiles[UserPredefinedFile] = "# USER_PREDEFINED.md\nOperator-authored user context"
meta := &ChannelMeta{ChannelType: "pancake", DisplayName: "Alice", DefaultTimezone: "Asia/Ho_Chi_Minh"}
seeded, err := SeedUserFiles(context.Background(), as, agentID, "user-fay", store.AgentTypePredefined, false, meta)
if err != nil {
t.Fatalf("SeedUserFiles returned error: %v", err)
}
for _, f := range seeded {
if f == UserFile {
t.Errorf("USER.md must not be seeded via channel prefill when USER_PREDEFINED.md exists, got seeded=%v", seeded)
}
}
if _, wrote := as.seededUserFiles[UserFile]; wrote {
t.Error("USER.md should not be written via channel prefill when USER_PREDEFINED.md exists")
}
}
// TestSeedUserFiles_OpenAgent_UsesEmbeddedTemplate verifies that open agents
// are completely unaffected — they still receive embedded templates per-user.
func TestSeedUserFiles_OpenAgent_UsesEmbeddedTemplate(t *testing.T) {