f521c583bd
The headline P4 piece (clean redesign): the Agent persona noun, decoupled from its Discord shell. - agent.go/storage.go/builtin_loader.go moved from mort's pkg/logic/agents; the Storage seam drops the Discord CommandBindingStorage embedding (a host concern). The host-entangled files (commands, chatbot_provider, command- binding dispatcher, personalization, system) stay in mort. - runnable.go: Agent.ToRunnable() lowers a persona into run.RunnableAgent — the bridge that lets run.Executor run a persona without importing this battery (the inversion of agentexec.Run(*agents.Agent)). - memory.go: NewMemory() — zero-dep in-process persona Storage (all 11 CRUD + trigger-query methods). Tests: ToRunnable field/phase mapping; Memory round-trip. CI invariant: core imports ZERO from persona. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
2.9 KiB
Go
121 lines
2.9 KiB
Go
package persona
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Memory is a zero-dependency in-process Storage for agent personas — a light
|
|
// host (or tests) gets persona persistence with no DB. Mort keeps its
|
|
// GORM/MySQL Storage; contrib/store adds a durable SQLite one.
|
|
type Memory struct {
|
|
mu sync.RWMutex
|
|
agents map[string]*Agent // by ID
|
|
}
|
|
|
|
// NewMemory returns an empty in-memory persona Storage.
|
|
func NewMemory() *Memory { return &Memory{agents: map[string]*Agent{}} }
|
|
|
|
var _ Storage = (*Memory)(nil)
|
|
|
|
func (m *Memory) InitializeAgentStorage(context.Context) error { return nil }
|
|
|
|
func (m *Memory) SaveAgent(_ context.Context, a *Agent) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
cp := *a
|
|
m.agents[a.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (m *Memory) GetAgent(_ context.Context, id string) (*Agent, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
a, ok := m.agents[id]
|
|
if !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
cp := *a
|
|
return &cp, nil
|
|
}
|
|
|
|
func (m *Memory) GetAgentByName(_ context.Context, ownerID, name string) (*Agent, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
for _, a := range m.agents {
|
|
if a.OwnerID == ownerID && a.Name == name {
|
|
cp := *a
|
|
return &cp, nil
|
|
}
|
|
}
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
func (m *Memory) listWhere(keep func(*Agent) bool) []*Agent {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
out := make([]*Agent, 0, len(m.agents))
|
|
for _, a := range m.agents {
|
|
if keep == nil || keep(a) {
|
|
cp := *a
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
func (m *Memory) ListAgents(_ context.Context, ownerID string) ([]*Agent, error) {
|
|
return m.listWhere(func(a *Agent) bool { return a.OwnerID == ownerID }), nil
|
|
}
|
|
|
|
func (m *Memory) ListAllAgents(context.Context) ([]*Agent, error) {
|
|
return m.listWhere(nil), nil
|
|
}
|
|
|
|
func (m *Memory) DeleteAgent(_ context.Context, id string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.agents, id)
|
|
return nil
|
|
}
|
|
|
|
func (m *Memory) GetAgentByWebhookSecret(_ context.Context, secret string) (*Agent, error) {
|
|
if secret == "" {
|
|
return nil, ErrNotFound
|
|
}
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
for _, a := range m.agents {
|
|
if a.WebhookSecret == secret {
|
|
cp := *a
|
|
return &cp, nil
|
|
}
|
|
}
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
func (m *Memory) ListAgentsByChatbotChannelFilter(context.Context) ([]*Agent, error) {
|
|
return m.listWhere(func(a *Agent) bool { return a.ChatbotChannelFilter != "" }), nil
|
|
}
|
|
|
|
func (m *Memory) ListScheduledAgents(_ context.Context, dueBefore time.Time) ([]*Agent, error) {
|
|
return m.listWhere(func(a *Agent) bool {
|
|
return a.Schedule != "" && a.NextRunAt != nil && !a.NextRunAt.After(dueBefore)
|
|
}), nil
|
|
}
|
|
|
|
func (m *Memory) MarkAgentScheduledRun(_ context.Context, agentID string, ranAt, nextAt time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
a, ok := m.agents[agentID]
|
|
if !ok {
|
|
return ErrNotFound
|
|
}
|
|
a.LastScheduledRunAt = &ranAt
|
|
a.NextRunAt = &nextAt
|
|
return nil
|
|
}
|