P4b: skill noun + contrib/store (SQLite for budget/persona/skill/audit)
executus CI / test (push) Has been cancelled
executus CI / test (push) Has been cancelled
Merges the skill half of the persona/skill pair plus the second nested module. (Squashed onto main from phase-4b-skill; the audit/budget/persona batteries it was stacked on already landed via the P4 merge.) - skill/: clean-redesign Skill noun + LEAN SkillStore (lifecycle/versions/ schedule only) + ToRunnable + Memory default. - contrib/store/: separate go.mod carrying modernc.org/sqlite, so the driver never enters the core go.sum. db.Budget()/Personas()/Skills()/Audit() back all four store seams (JSON-blob + indexed columns; round-trip tested). Includes the verified gadfly #5 fixes (AppendVersion tx+UNIQUE+error, Mark*ScheduledRun atomic json_set, busy_timeout, NaN guard). - CI: builds + tests the nested module and asserts it owns the sqlite driver. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user