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>
192 lines
8.5 KiB
Go
192 lines
8.5 KiB
Go
// Package agents implements the Agent noun: a persisted persona +
|
||
// execution spec + palette of skills/sub-agents/low-level tools, with
|
||
// optional trigger metadata (schedule, webhook, chatbot channel
|
||
// listener) and personalization sources.
|
||
//
|
||
// Phase 1 scope: this package introduces Agent as a persisted noun
|
||
// with CRUD only — no execution path, no palette resolution, no
|
||
// trigger handling. See /Users/steve/.claude/plans/serene-churning-micali.md
|
||
// for the staged rollout. Later phases add agentexec, agent_invoke,
|
||
// trigger dispatch (schedule/webhook/chatbot), and CommandBinding.
|
||
//
|
||
// The three-layer storage pattern from pkg/logic/storage/CLAUDE.md
|
||
// applies — when adding a field to Agent, you MUST update
|
||
// pkg/logic/storage/agents.go (gormAgent, agentFromStorage,
|
||
// toStorage) or persistence will silently break.
|
||
package persona
|
||
|
||
import "time"
|
||
|
||
// Agent is the domain definition of an Agent persona + execution spec.
|
||
//
|
||
// Why: an Agent is the "configured invoker" — model tier + base
|
||
// system prompt + a palette of capabilities (skills, sub-agents,
|
||
// low-level tools) it may exercise during a run. Where a Skill is a
|
||
// reusable parameterised callable (a library function), an Agent is
|
||
// the actor with a persistent persona that calls those skills. The
|
||
// struct is flat — every field lives on its own column on the
|
||
// agents table; JSON columns are used only for variable-length
|
||
// collections (palette lists, tags, etc.).
|
||
//
|
||
// What: identity + persona + execution caps + palette + triggers +
|
||
// personalization + UX, all on one struct. Several field families
|
||
// (Palette, Triggers, Personalization) are persisted now but NOT
|
||
// exercised until later phases — they exist so the schema is stable
|
||
// and future phases can light up behaviour without DB migrations.
|
||
//
|
||
// Test: see pkg/logic/agents/storage_round_trip_test.go for
|
||
// Save/Get/GetByName/List/Delete coverage.
|
||
type Agent struct {
|
||
// Identity
|
||
ID string // UUID
|
||
Name string // unique per OwnerID; human-friendly identifier
|
||
Description string
|
||
OwnerID string // Discord member ID
|
||
AuthoredBy string // Discord member ID; usually == OwnerID
|
||
Version int // monotonic, for future versioning
|
||
CreatedAt time.Time
|
||
UpdatedAt time.Time
|
||
|
||
// Extends names the parent agent this agent inherits from. Only used
|
||
// during builtin loading — the loader resolves extends references and
|
||
// merges fields before persisting. The resolved agent is a standalone
|
||
// entity; Extends is NOT persisted in the database. Only single-level
|
||
// extends is supported (no chains).
|
||
Extends string
|
||
|
||
// SystemPromptPrepend, when non-empty, is prepended to the system
|
||
// prompt (with a trailing newline separator). Used by the extends
|
||
// mechanism so a child agent can prepend persona instructions to the
|
||
// parent's full system prompt without duplicating it. Like Extends,
|
||
// this is resolved at load time and NOT persisted — the final
|
||
// SystemPrompt on the persisted Agent already has the prepend applied.
|
||
SystemPromptPrepend string
|
||
|
||
// Persona / execution spec
|
||
ModelTier string // "fast" | "standard" | "thinking" | provider/model
|
||
SystemPrompt string // base persona prompt (Phase 5 layers personalization on top)
|
||
MaxIterations int // 0 → use convar default at execution time
|
||
MaxToolCalls int // 0 → use convar default at execution time
|
||
MaxRuntime time.Duration // stored as MaxRuntimeNs int64 in GORM (avoid duration-driver flakiness)
|
||
ExecutionLane string // lane name; empty = default at execution time
|
||
EncryptionEnabled bool // Phase 1 stores the flag; envelope-encryption bridge wires in a later phase
|
||
|
||
// Run-critic (two-tier timeout). When CriticEnabled is false (the
|
||
// default) MaxRuntime is the hard kill, exactly as before. When true,
|
||
// MaxRuntime becomes a SOFT trigger: at MaxRuntime the run-critic
|
||
// activates and periodically reviews the run; the hard backstop (the
|
||
// absolute kill) is MaxRuntime × the multiplier. CriticBackstopMultiplier
|
||
// of 0 means "use the convar default" (agents.critic.backstop_multiplier_default,
|
||
// default 6×). See pkg/logic/agentcritic.
|
||
CriticEnabled bool
|
||
CriticBackstopMultiplier float64
|
||
|
||
// Palette — what this Agent may invoke (Phase 2 reads these).
|
||
// Stored as JSON arrays; not exercised by Phase 1 CRUD.
|
||
SkillPalette []string // skill IDs/names
|
||
SubAgentPalette []string // agent IDs/names
|
||
LowLevelTools []string // skilltools registry names
|
||
|
||
// Personalization (Phase 5 reads these). Each layer name maps to
|
||
// a registered PersonalizationProvider that returns text appended
|
||
// to SystemPrompt at run time. Empty list = base prompt only.
|
||
PersonalizationSources []string
|
||
|
||
// Triggers — persisted now, NOT dispatched by Phase 1.
|
||
//
|
||
// Schedule: cron expression or "daily"/"weekly" shorthand. Empty
|
||
// = on-demand only. NextRunAt + LastScheduledRunAt are scheduler
|
||
// bookkeeping for Phase 3's per-Agent scheduler.
|
||
Schedule string
|
||
NextRunAt *time.Time
|
||
LastScheduledRunAt *time.Time
|
||
|
||
// Webhook trigger metadata. WebhookSecret empty = inbound
|
||
// webhooks disabled. WebhookSignatureRequired defaults true at
|
||
// save time (see Skill's lesson: don't store a GORM default on a
|
||
// bool where false is a legitimate explicit value — application
|
||
// layer is the source of truth).
|
||
WebhookSecret string
|
||
WebhookSignatureRequired bool
|
||
WebhookIPAllowlist []string // CIDR strings; stored as JSON array
|
||
|
||
// Chatbot trigger metadata. ChatbotChannelFilter names a filter
|
||
// registered in pkg/logic/skills' ChannelFilterRegistry — when
|
||
// the migrated chatbot dispatches via this Agent, the filter
|
||
// gates which channels it listens in.
|
||
ChatbotChannelFilter string
|
||
|
||
// UX
|
||
//
|
||
// DefaultEmoji is an optional identity emoji for this agent.
|
||
// Used as the __start__ fallback and forwarded to the invoking
|
||
// Discord message when a parent calls this agent via agent_invoke.
|
||
DefaultEmoji string
|
||
|
||
// StateReactEmoji maps tool names (and reserved keys "__start__",
|
||
// "__end__", "__error__") to Discord emoji that the executor
|
||
// reacts with as the run progresses. Empty map = no reactions.
|
||
StateReactEmoji map[string]string
|
||
|
||
// Tags is a free-form set of short labels for organisation +
|
||
// discovery on the agents list page (Phase 1 admin commands +
|
||
// future web UI).
|
||
Tags []string
|
||
|
||
// Phases defines a multi-phase pipeline for this agent. When
|
||
// non-empty, the executor runs agentexec's sequential phase runner
|
||
// instead of the single agent loop. Empty = single-loop agent.
|
||
//
|
||
// Phases IS persisted (JSON struct-slice column `phases` on
|
||
// gormAgent). It used to be transient — "TOML is the only source of
|
||
// truth" — but every production dispatch path resolves the agent from
|
||
// the DB, where the dropped Phases meant research / deepresearch
|
||
// silently degraded to a single-loop run (the executor's
|
||
// `len(a.Phases) > 0` pipeline branch was dead). The builtin loader
|
||
// still seeds phases from YAML; persisting them is what makes the
|
||
// pipeline branch fire for DB-loaded agents.
|
||
Phases []AgentPhase
|
||
}
|
||
|
||
// AgentPhase describes one stage of a multi-phase pipeline in an
|
||
// agent definition. Executed directly by agentexec's phase runner
|
||
// (pipeline.go) — there is no intermediate execution-spec struct.
|
||
//
|
||
// What: name + prompt template + model/iteration overrides + tool
|
||
// list + optional/fallback flags + IsRunFunc indicator.
|
||
//
|
||
// Test: see builtin_loader_test.go for YAML round-trip coverage.
|
||
type AgentPhase struct {
|
||
// Name identifies the phase (e.g., "scout", "plan", "investigate").
|
||
Name string
|
||
|
||
// SystemPrompt for this phase. Supports template variables:
|
||
// {{.Query}} for the original query, and {{.<PhaseName>}} for
|
||
// prior phase outputs (e.g., {{.scout}}, {{.plan}}).
|
||
SystemPrompt string
|
||
|
||
// ModelTier overrides the agent's ModelTier for this phase.
|
||
// Empty = use agent default.
|
||
ModelTier string
|
||
|
||
// MaxIter overrides the agent's MaxIterations for this phase.
|
||
// 0 = use agent default.
|
||
MaxIter int
|
||
|
||
// Tools are tool names for this phase only. These are resolved
|
||
// from the agent's low-level tools + palette at execution time.
|
||
Tools []string
|
||
|
||
// Optional means errors in this phase don't abort the pipeline.
|
||
Optional bool
|
||
|
||
// FallbackMessage is used when an optional phase fails.
|
||
// Default: "(Phase <Name> encountered an error)"
|
||
FallbackMessage string
|
||
|
||
// IsRunFunc indicates this phase is a bare LLM call (no tool
|
||
// loop). When true, the executor makes a single model.Complete
|
||
// call instead of running the full agent loop.
|
||
IsRunFunc bool
|
||
}
|