2260480c81
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>
116 lines
5.0 KiB
Go
116 lines
5.0 KiB
Go
package persona
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// ErrNotFound is returned when an agent lookup fails. Callers compare
|
|
// with errors.Is(err, ErrNotFound).
|
|
var ErrNotFound = errors.New("agent not found")
|
|
|
|
// Storage is the persistence interface for the agents system.
|
|
//
|
|
// Why: tests substitute fake implementations; production wires
|
|
// through pkg/logic/storage's Grand Storage which embeds this
|
|
// interface. Mirrors the three-layer pattern in
|
|
// pkg/logic/storage/CLAUDE.md (domain → GORM → DB).
|
|
//
|
|
// What: Phase 1 CRUD plus Phase 3 trigger queries
|
|
// (ListDueScheduled, GetAgentByWebhookSecret,
|
|
// ListAgentsByChatbotChannelFilter, MarkScheduledRun). Trigger
|
|
// queries are read by the agentsched runner, webhook router, and
|
|
// chatbot tool provider; all are gated behind the
|
|
// agents.triggers.enabled convar so old skill-driven paths keep
|
|
// running until the convar flips.
|
|
//
|
|
// Test: see storage_round_trip_test.go for round-trip coverage.
|
|
type Storage interface {
|
|
// (Mort's Discord command-binding CRUD — the CommandBindingStorage
|
|
// embedding — stays a host concern and is NOT part of the executus
|
|
// persona Storage seam.)
|
|
|
|
// InitializeAgentStorage prepares storage (e.g. AutoMigrate)
|
|
// and is idempotent. Called from the grand storage's
|
|
// InitializeAll path.
|
|
InitializeAgentStorage(ctx context.Context) error
|
|
|
|
// SaveAgent creates or updates an Agent by ID. ID must be
|
|
// non-empty (Phase 1 admin commands mint a UUID).
|
|
SaveAgent(ctx context.Context, a *Agent) error
|
|
|
|
// GetAgent returns the agent with the given ID, or ErrNotFound.
|
|
GetAgent(ctx context.Context, id string) (*Agent, error)
|
|
|
|
// GetAgentByName resolves (owner_id, name) → agent. ownerID
|
|
// must match exactly (Phase 1 has no shared/public visibility
|
|
// yet; every agent is owned).
|
|
GetAgentByName(ctx context.Context, ownerID, name string) (*Agent, error)
|
|
|
|
// ListAgents returns every agent owned by the given member ID,
|
|
// sorted by Name ASC.
|
|
ListAgents(ctx context.Context, ownerID string) ([]*Agent, error)
|
|
|
|
// ListAllAgents returns every agent across all owners, sorted by
|
|
// (OwnerID ASC, Name ASC) so builtin rows (OwnerID="builtin")
|
|
// group together, then numeric Discord-ID owners in lexical order,
|
|
// then chatbot-shadow rows whose OwnerID is the chatbot owner's
|
|
// Discord ID but whose Name carries the "chatbot:" prefix.
|
|
//
|
|
// Why: Phase 1 admin commands ran owner-scoped (a steve-owned
|
|
// agent list shows ONLY steve's rows), which hid builtin and
|
|
// shadow Agents from the admin view. `.agent list` for admins now
|
|
// uses this method to surface every row. Non-admin invocations
|
|
// (or `.agent list --mine`) keep using ListAgents.
|
|
//
|
|
// Storage MAY back this with a single full-table scan — admin
|
|
// row counts are small (dozens to low hundreds), so no need for
|
|
// pagination at this phase.
|
|
ListAllAgents(ctx context.Context) ([]*Agent, error)
|
|
|
|
// DeleteAgent removes an agent by ID. Idempotent — deleting a
|
|
// missing row returns nil.
|
|
DeleteAgent(ctx context.Context, id string) error
|
|
|
|
// GetAgentByWebhookSecret resolves a posted /webhooks/<secret> URL
|
|
// to the matching agent. Returns ErrNotFound when no agent has
|
|
// the secret. Phase 3 webhook router consults this AFTER the
|
|
// existing Skill lookup falls through, but only when
|
|
// agents.triggers.enabled is true.
|
|
//
|
|
// Empty secret is rejected with ErrNotFound (empty WebhookSecret
|
|
// rows are NOT webhook-enabled — the application layer guards
|
|
// this, the lookup defends against accidental match).
|
|
GetAgentByWebhookSecret(ctx context.Context, secret string) (*Agent, error)
|
|
|
|
// ListAgentsByChatbotChannelFilter returns every agent with a
|
|
// non-empty ChatbotChannelFilter. Phase 3 chatbot tool provider
|
|
// uses this on every chatbot turn to assemble the per-channel
|
|
// tool list (gated by agents.triggers.enabled). The result is
|
|
// not channel-filtered here — the provider applies the channel
|
|
// filter predicate (registered in skills.ChannelFilterRegistry)
|
|
// to each row.
|
|
//
|
|
// Why no channel filter at the storage layer: the filter is a
|
|
// runtime predicate (e.g. dm_only depends on the live Discord
|
|
// channel kind cache), not a static column we can index on.
|
|
ListAgentsByChatbotChannelFilter(ctx context.Context) ([]*Agent, error)
|
|
|
|
// ListScheduledAgents returns every agent with a non-empty
|
|
// Schedule whose NextRunAt is at or before `dueBefore`. Result
|
|
// is ordered by NextRunAt ASC so the scheduler runner can drain
|
|
// in oldest-due-first order. Mirrors skills.Storage.ListDueScheduled.
|
|
//
|
|
// Phase 3 scheduler reads this on every tick when
|
|
// agents.triggers.enabled is true. The (Schedule, NextRunAt)
|
|
// composite index backs the query — see gorm tags on gormAgent.
|
|
ListScheduledAgents(ctx context.Context, dueBefore time.Time) ([]*Agent, error)
|
|
|
|
// MarkAgentScheduledRun atomically updates LastScheduledRunAt
|
|
// and NextRunAt for the given agent. Called by the agentsched
|
|
// runner after each scheduled invocation. Mirrors
|
|
// skills.Storage.MarkScheduledRun semantics.
|
|
MarkAgentScheduledRun(ctx context.Context, agentID string, ranAt, nextAt time.Time) error
|
|
}
|