// 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 {{.}} 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 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 }