Files
executus/skill/memory.go
T
steve 41659b2412 P4: skill noun — domain + LEAN SkillStore + ToRunnable + Memory
The skill half of the persona/skill pair, as a clean redesign (not a faithful
lift of mort's 60-method skills.Storage kitchen sink):
- skill.go/skill_version.go/validate.go/inputs.go/schedule.go moved clean; the
  only host couplings severed: llms.IsTierName -> model.IsTierName, and the
  chatbot DefaultChatbotInputName const localized.
- store.go: a DELIBERATELY LEAN SkillStore — skill lifecycle (CRUD + visibility)
  + versioning + scheduling ONLY. The KV/file/quota sub-stores that were fused
  into mort's interface are the tools/ store seams; email/channel grants stay
  host concerns.
- runnable.go: Skill.ToRunnable() lowers a skill into run.RunnableAgent (flat
  tool list, no palette — composition is a host concern); DueAt() helper.
- memory.go: NewMemory() — zero-dep in-process SkillStore (visibility filters,
  newest-first versions).

Tests: ToRunnable mapping, visibility (public/shared/private) listing, version
ordering + lookup. No mort dependency (go.mod tidy clean); core imports ZERO
from skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:40:58 -04:00

170 lines
4.0 KiB
Go

package skill
import (
"context"
"sort"
"sync"
"time"
)
// Memory is a zero-dependency in-process SkillStore — a light host or test gets
// saved-skill persistence with no DB. Mort backs SkillStore with GORM/MySQL;
// contrib/store adds durable SQLite.
type Memory struct {
mu sync.RWMutex
skills map[string]*Skill // by ID
versions map[string][]SkillVersion // by skill ID, append order
byVerID map[string]SkillVersion // by version ID
}
// NewMemory returns an empty in-memory SkillStore.
func NewMemory() *Memory {
return &Memory{
skills: map[string]*Skill{},
versions: map[string][]SkillVersion{},
byVerID: map[string]SkillVersion{},
}
}
var _ SkillStore = (*Memory)(nil)
func (m *Memory) Initialize(context.Context) error { return nil }
func (m *Memory) Save(_ context.Context, s *Skill) error {
m.mu.Lock()
defer m.mu.Unlock()
cp := *s
m.skills[s.ID] = &cp
return nil
}
func (m *Memory) Get(_ context.Context, id string) (*Skill, error) {
m.mu.RLock()
defer m.mu.RUnlock()
s, ok := m.skills[id]
if !ok {
return nil, ErrNotFound
}
cp := *s
return &cp, nil
}
func (m *Memory) GetByName(_ context.Context, ownerID, name string) (*Skill, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, s := range m.skills {
if s.OwnerID == ownerID && s.Name == name {
cp := *s
return &cp, nil
}
}
return nil, ErrNotFound
}
func (m *Memory) Delete(_ context.Context, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.skills, id)
return nil
}
func (m *Memory) listWhere(keep func(*Skill) bool) []Skill {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]Skill, 0, len(m.skills))
for _, s := range m.skills {
if keep == nil || keep(s) {
out = append(out, *s)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
func (m *Memory) ListByOwner(_ context.Context, ownerID string) ([]Skill, error) {
return m.listWhere(func(s *Skill) bool { return s.OwnerID == ownerID }), nil
}
func (m *Memory) ListPublic(context.Context) ([]Skill, error) {
return m.listWhere(func(s *Skill) bool { return s.Visibility == VisibilityPublic }), nil
}
func (m *Memory) ListSharedWith(_ context.Context, memberID string) ([]Skill, error) {
return m.listWhere(func(s *Skill) bool {
if s.Visibility != VisibilityShared {
return false
}
for _, id := range s.SharedWith {
if id == memberID {
return true
}
}
return false
}), nil
}
func (m *Memory) ListBuiltinByName(_ context.Context, name string) (*Skill, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, s := range m.skills {
if s.Source == SourceBuiltin && s.Name == name {
cp := *s
return &cp, nil
}
}
return nil, ErrNotFound
}
func (m *Memory) ListChatbotExposed(context.Context) ([]Skill, error) {
return m.listWhere(func(s *Skill) bool { return s.ExposeAsChatbotTool }), nil
}
func (m *Memory) ListDueScheduled(_ context.Context, now time.Time) ([]Skill, error) {
return m.listWhere(func(s *Skill) bool { return s.DueAt(now) }), nil
}
func (m *Memory) MarkScheduledRun(_ context.Context, skillID string, ranAt, nextAt time.Time) error {
m.mu.Lock()
defer m.mu.Unlock()
s, ok := m.skills[skillID]
if !ok {
return ErrNotFound
}
s.LastScheduledRunAt = ranAt
s.NextRunAt = nextAt
return nil
}
func (m *Memory) AppendVersion(_ context.Context, sv SkillVersion) error {
m.mu.Lock()
defer m.mu.Unlock()
m.versions[sv.SkillID] = append(m.versions[sv.SkillID], sv)
m.byVerID[sv.ID] = sv
return nil
}
func (m *Memory) ListVersionsBySkill(_ context.Context, skillID string, limit int) ([]SkillVersion, error) {
m.mu.RLock()
defer m.mu.RUnlock()
all := m.versions[skillID]
// newest first
out := make([]SkillVersion, 0, len(all))
for i := len(all) - 1; i >= 0; i-- {
out = append(out, all[i])
if limit > 0 && len(out) >= limit {
break
}
}
return out, nil
}
func (m *Memory) GetVersionByID(_ context.Context, versionID string) (*SkillVersion, error) {
m.mu.RLock()
defer m.mu.RUnlock()
sv, ok := m.byVerID[versionID]
if !ok {
return nil, ErrNotFound
}
return &sv, nil
}