mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 02:14:43 +00:00
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:
1 parent
6bafe54b08
commit
d3c04ab3e5
4 files changed
+169
-22
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user