feat: seed initial memories from org template and create payload (#1050)
Add MemorySeed model and initial_memories support at three levels: - POST /workspaces payload: seed memories on workspace creation - org.yaml workspace config: per-workspace initial_memories with defaults fallback - org.yaml global_memories: org-wide GLOBAL scope memories seeded on the first root workspace during import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e345aa832a
commit
ff7ac87b97
@ -74,10 +74,13 @@ func NewOrgHandler(wh *WorkspaceHandler, b *events.Broadcaster, p *provisioner.P
|
||||
|
||||
// OrgTemplate is the YAML structure for an org hierarchy.
|
||||
type OrgTemplate struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Defaults OrgDefaults `yaml:"defaults" json:"defaults"`
|
||||
Workspaces []OrgWorkspace `yaml:"workspaces" json:"workspaces"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Defaults OrgDefaults `yaml:"defaults" json:"defaults"`
|
||||
Workspaces []OrgWorkspace `yaml:"workspaces" json:"workspaces"`
|
||||
// GlobalMemories is a list of org-wide memories seeded as GLOBAL scope
|
||||
// on the first root workspace (PM) during org import. Issue #1050.
|
||||
GlobalMemories []models.MemorySeed `yaml:"global_memories" json:"global_memories"`
|
||||
}
|
||||
|
||||
type OrgDefaults struct {
|
||||
@ -106,6 +109,9 @@ type OrgDefaults struct {
|
||||
// Rendered into each workspace's config.yaml so agent prompts can read it
|
||||
// generically (no hardcoded role names in prompts). See issue #51.
|
||||
CategoryRouting map[string][]string `yaml:"category_routing" json:"category_routing"`
|
||||
// InitialMemories are default memories seeded into every workspace at
|
||||
// creation time unless the workspace overrides them. Issue #1050.
|
||||
InitialMemories []models.MemorySeed `yaml:"initial_memories" json:"initial_memories"`
|
||||
}
|
||||
|
||||
type OrgSchedule struct {
|
||||
@ -170,6 +176,9 @@ type OrgWorkspace struct {
|
||||
// (empty list drops the category entirely); new keys are added. See
|
||||
// mergeCategoryRouting.
|
||||
CategoryRouting map[string][]string `yaml:"category_routing" json:"category_routing"`
|
||||
// InitialMemories are memories seeded into this workspace at creation
|
||||
// time. If empty, defaults.initial_memories are used. Issue #1050.
|
||||
InitialMemories []models.MemorySeed `yaml:"initial_memories" json:"initial_memories"`
|
||||
Schedules []OrgSchedule `yaml:"schedules" json:"schedules"`
|
||||
Channels []OrgChannel `yaml:"channels" json:"channels"`
|
||||
External bool `yaml:"external" json:"external"`
|
||||
@ -290,6 +299,22 @@ func (h *OrgHandler) Import(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Seed org-wide global_memories on the first root workspace (issue #1050).
|
||||
// These are GLOBAL scope memories visible to all workspaces in the org.
|
||||
if len(tmpl.GlobalMemories) > 0 && len(results) > 0 {
|
||||
rootID, _ := results[0]["id"].(string)
|
||||
if rootID != "" {
|
||||
rootNS := workspaceAwarenessNamespace(rootID)
|
||||
// Force scope to GLOBAL regardless of what the YAML says.
|
||||
globalSeeds := make([]models.MemorySeed, len(tmpl.GlobalMemories))
|
||||
for i, gm := range tmpl.GlobalMemories {
|
||||
globalSeeds[i] = models.MemorySeed{Content: gm.Content, Scope: "GLOBAL"}
|
||||
}
|
||||
seedInitialMemories(context.Background(), rootID, globalSeeds, rootNS)
|
||||
log.Printf("Org import: seeded %d global memories on root workspace %s", len(globalSeeds), rootID)
|
||||
}
|
||||
}
|
||||
|
||||
// Hot-reload channel manager once after all channels are inserted
|
||||
// (instead of per-workspace, avoiding N redundant DB queries + diffs).
|
||||
if h.channelMgr != nil {
|
||||
@ -408,6 +433,15 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa
|
||||
"name": ws.Name, "tier": tier,
|
||||
})
|
||||
|
||||
// Seed initial memories from workspace config or defaults (issue #1050).
|
||||
// Per-workspace initial_memories override defaults; if workspace has none,
|
||||
// fall back to defaults.initial_memories.
|
||||
wsMemories := ws.InitialMemories
|
||||
if len(wsMemories) == 0 {
|
||||
wsMemories = defaults.InitialMemories
|
||||
}
|
||||
seedInitialMemories(ctx, id, wsMemories, awarenessNS)
|
||||
|
||||
// Handle external workspaces
|
||||
if ws.External {
|
||||
if _, err := db.DB.ExecContext(ctx, `UPDATE workspaces SET status = 'online', url = $1 WHERE id = $2`, ws.URL, id); err != nil {
|
||||
|
||||
@ -209,6 +209,10 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
||||
log.Printf("Create: canvas layout insert failed for %s (workspace will appear at 0,0): %v", id, err)
|
||||
}
|
||||
|
||||
// Seed initial memories from the create payload (issue #1050).
|
||||
// Non-fatal: failures are logged but don't block workspace creation.
|
||||
seedInitialMemories(ctx, id, payload.InitialMemories, awarenessNamespace)
|
||||
|
||||
// Broadcast provisioning event
|
||||
h.broadcaster.RecordAndBroadcast(ctx, "WORKSPACE_PROVISIONING", id, map[string]interface{}{
|
||||
"name": payload.Name,
|
||||
|
||||
@ -187,6 +187,36 @@ func (h *WorkspaceHandler) provisionWorkspaceOpts(workspaceID, templatePath stri
|
||||
// which transitions status to 'online' and broadcasts WORKSPACE_ONLINE
|
||||
}
|
||||
|
||||
// seedInitialMemories inserts a list of MemorySeed entries into agent_memories
|
||||
// for the given workspace. Called during workspace creation and org import to
|
||||
// pre-populate memories from config/template. Non-fatal: each insert is
|
||||
// attempted independently and failures are logged. Issue #1050.
|
||||
func seedInitialMemories(ctx context.Context, workspaceID string, memories []models.MemorySeed, awarenessNamespace string) {
|
||||
if len(memories) == 0 {
|
||||
return
|
||||
}
|
||||
for _, mem := range memories {
|
||||
scope := strings.ToUpper(mem.Scope)
|
||||
if scope == "" {
|
||||
scope = "LOCAL"
|
||||
}
|
||||
if scope != "LOCAL" && scope != "TEAM" && scope != "GLOBAL" {
|
||||
log.Printf("seedInitialMemories: skipping memory for %s — invalid scope %q", workspaceID, scope)
|
||||
continue
|
||||
}
|
||||
if mem.Content == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := db.DB.ExecContext(ctx, `
|
||||
INSERT INTO agent_memories (workspace_id, content, scope, namespace)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, workspaceID, mem.Content, scope, awarenessNamespace); err != nil {
|
||||
log.Printf("seedInitialMemories: failed to insert memory for %s (scope=%s): %v", workspaceID, scope, err)
|
||||
}
|
||||
}
|
||||
log.Printf("seedInitialMemories: seeded %d memories for workspace %s", len(memories), workspaceID)
|
||||
}
|
||||
|
||||
func workspaceAwarenessNamespace(workspaceID string) string {
|
||||
return fmt.Sprintf("workspace:%s", workspaceID)
|
||||
}
|
||||
|
||||
@ -57,6 +57,14 @@ type UpdateCardPayload struct {
|
||||
AgentCard json.RawMessage `json:"agent_card" binding:"required"`
|
||||
}
|
||||
|
||||
// MemorySeed represents an initial memory to seed into a workspace at creation time.
|
||||
// Used by both the POST /workspaces API and org template import to pre-populate
|
||||
// agent memories from config (issue #1050).
|
||||
type MemorySeed struct {
|
||||
Content string `json:"content" yaml:"content"`
|
||||
Scope string `json:"scope" yaml:"scope"` // LOCAL, TEAM, GLOBAL
|
||||
}
|
||||
|
||||
type CreateWorkspacePayload struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Role string `json:"role"`
|
||||
@ -80,6 +88,10 @@ type CreateWorkspacePayload struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
} `json:"canvas"`
|
||||
// InitialMemories is an optional list of memories to seed into the
|
||||
// workspace immediately after creation. Each entry is inserted into
|
||||
// agent_memories with the workspace's awareness namespace. Issue #1050.
|
||||
InitialMemories []MemorySeed `json:"initial_memories"`
|
||||
}
|
||||
|
||||
type CheckAccessPayload struct {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user