// Package registry implements the character library and persona registration // for the persona system. A character comes from the character library, binds // exactly one persona, and declares the behavior modes it is good at; the // registered persona additionally owns a per-persona memory isolation namespace // and the emotion/affection state that follows the persona. package registry import ( "fmt" "sort" "sync" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/emotion" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader" ) // Character is an entity from the character library. It binds one persona and // declares the behavior modes it excels at (as a suggestion, never an ability // bound — see DevDocs/design/01). type Character struct { // ID is the unique character identifier. ID string // Name is the display name of the character. Name string // Default marks this character as the library's default role. Default bool // PersonaID is the persona bound to this character. PersonaID string // Modes lists the behavior modes the character is good at. Modes []string } // Registry is the in-memory, concurrency-safe character library and persona // registry. It also owns the emotion Manager so that emotion/affection always // follow the registered persona. type Registry struct { mu sync.RWMutex personas map[string]*loader.Persona characters map[string]*Character defaultID string emotions *emotion.Manager } // New returns an empty Registry. func New() *Registry { return &Registry{ personas: make(map[string]*loader.Persona), characters: make(map[string]*Character), emotions: emotion.NewManager(), } } // RegisterPersona registers p and builds its emotion machine and affection // tracker. It fails when p is nil, has no id, is already registered, or its // emotion/affection config is invalid. func (r *Registry) RegisterPersona(p *loader.Persona) error { if p == nil { return fmt.Errorf("registry: nil persona") } if p.ID == "" { return fmt.Errorf("registry: persona id required") } machine, err := p.NewMachine() if err != nil { return fmt.Errorf("registry: persona %q machine: %w", p.ID, err) } affection, err := p.NewAffection() if err != nil { return fmt.Errorf("registry: persona %q affection: %w", p.ID, err) } r.mu.Lock() if _, ok := r.personas[p.ID]; ok { r.mu.Unlock() return fmt.Errorf("registry: persona %q already registered", p.ID) } r.personas[p.ID] = p r.mu.Unlock() r.emotions.Set(p.ID, machine, affection) return nil } // Persona returns the persona registered under id. func (r *Registry) Persona(id string) (*loader.Persona, bool) { r.mu.RLock() defer r.mu.RUnlock() p, ok := r.personas[id] return p, ok } // UpdatePersona replaces the persona registered under p.ID with p, rebuilding // its emotion machine and affection tracker. It fails when p is nil, has no id, // or is not currently registered. In-flight loops bound to the previous // *loader.Persona keep their pointer; only future lookups observe the new value. func (r *Registry) UpdatePersona(p *loader.Persona) error { if p == nil { return fmt.Errorf("registry: nil persona") } if p.ID == "" { return fmt.Errorf("registry: persona id required") } machine, err := p.NewMachine() if err != nil { return fmt.Errorf("registry: persona %q machine: %w", p.ID, err) } affection, err := p.NewAffection() if err != nil { return fmt.Errorf("registry: persona %q affection: %w", p.ID, err) } r.mu.Lock() if _, ok := r.personas[p.ID]; !ok { r.mu.Unlock() return fmt.Errorf("registry: persona %q not registered", p.ID) } r.personas[p.ID] = p r.mu.Unlock() r.emotions.Set(p.ID, machine, affection) return nil } // UpdateCharacter replaces the fields of the character registered under c.ID, // leaving its Default flag intact unless c re-declares it. It fails when c is // nil, has no id, is not registered, or its persona does not exist. func (r *Registry) UpdateCharacter(c *Character) error { if c == nil { return fmt.Errorf("registry: nil character") } if c.ID == "" { return fmt.Errorf("registry: character id required") } r.mu.Lock() defer r.mu.Unlock() existing, ok := r.characters[c.ID] if !ok { return fmt.Errorf("registry: character %q not registered", c.ID) } if _, ok := r.personas[c.PersonaID]; !ok { return fmt.Errorf("registry: character %q references unknown persona %q", c.ID, c.PersonaID) } existing.Name = c.Name existing.PersonaID = c.PersonaID existing.Modes = append(existing.Modes[:0], c.Modes...) if c.Default { existing.Default = true r.defaultID = c.ID } return nil } // RegisterCharacter registers c. It requires c's persona to exist, so persona // registration must happen first. A character marked Default becomes the // library's default role. func (r *Registry) RegisterCharacter(c *Character) error { if c == nil { return fmt.Errorf("registry: nil character") } if c.ID == "" { return fmt.Errorf("registry: character id required") } if c.Name == "" { return fmt.Errorf("registry: character %q name required", c.ID) } if c.PersonaID == "" { return fmt.Errorf("registry: character %q personaID required", c.ID) } r.mu.Lock() defer r.mu.Unlock() if _, ok := r.characters[c.ID]; ok { return fmt.Errorf("registry: character %q already registered", c.ID) } if _, ok := r.personas[c.PersonaID]; !ok { return fmt.Errorf("registry: character %q references unknown persona %q", c.ID, c.PersonaID) } r.characters[c.ID] = c if c.Default { r.defaultID = c.ID } return nil } // Character returns the character registered under id. func (r *Registry) Character(id string) (*Character, bool) { r.mu.RLock() defer r.mu.RUnlock() c, ok := r.characters[id] return c, ok } // SetDefaultCharacter makes the character with id the library default. It fails // when id is not a registered character. func (r *Registry) SetDefaultCharacter(id string) error { r.mu.Lock() defer r.mu.Unlock() c, ok := r.characters[id] if !ok { return fmt.Errorf("registry: set default to unknown character %q", id) } c.Default = true r.defaultID = id return nil } // DefaultCharacter returns the library's default character, if one is set. func (r *Registry) DefaultCharacter() (*Character, bool) { r.mu.RLock() defer r.mu.RUnlock() if r.defaultID == "" { return nil, false } c, ok := r.characters[r.defaultID] return c, ok } // MemoryNamespace returns the per-persona memory isolation namespace for the // persona under personaID, guarding memory isolation between personas. func (r *Registry) MemoryNamespace(personaID string) (string, bool) { r.mu.RLock() defer r.mu.RUnlock() p, ok := r.personas[personaID] if !ok { return "", false } return p.MemoryNamespace(), true } // Emotions returns the emotion Manager that owns every registered persona's // emotion and affection state. func (r *Registry) Emotions() *emotion.Manager { return r.emotions } // AffectionStep returns the per-turn affection magnitude configured for the // persona under personaID (its 好感度曲线 delta, design/01 §2). It returns 0 for // an unknown persona, so a caller skips the adjustment instead of inventing one. func (r *Registry) AffectionStep(personaID string) int { if p, ok := r.Persona(personaID); ok { return p.AffectionStep() } return 0 } // CountPersonas returns the number of registered personas. func (r *Registry) CountPersonas() int { r.mu.RLock() defer r.mu.RUnlock() return len(r.personas) } // CountCharacters returns the number of registered characters. func (r *Registry) CountCharacters() int { r.mu.RLock() defer r.mu.RUnlock() return len(r.characters) } // List returns every registered character, sorted by ID. Each returned Character // is a copy so callers cannot mutate the registry's internal state. It is safe // for concurrent use. func (r *Registry) List() []Character { r.mu.RLock() defer r.mu.RUnlock() out := make([]Character, 0, len(r.characters)) for _, c := range r.characters { ch := *c ch.Modes = append([]string(nil), c.Modes...) out = append(out, ch) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out }