import { Logger } from '@happyvertical/logger'; import { ConfigResolver, DispatchBus, DispatchMetadata, LearningEpisode, LearningMemory, LearningMemoryRecord, LearningOutcome, LearningSemanticSearch, SmrtObject, SmrtObjectOptions } from '@happyvertical/smrt-core'; import { AgentAIOptions } from './ai-config.js'; import { AgentWithInterestsOptions, InterestOptions, InterestResult, ObjectFilter } from './interests.js'; import { AgentLearningDeclaration } from './learning.js'; import { AgentStatusType } from './types.js'; import { AgentAdminRoute, AgentUISlots } from './ui.js'; /** * Agent constructor options */ export interface AgentOptions extends SmrtObjectOptions, AgentWithInterestsOptions { /** * Optional AI configuration for this agent. * * When `apiKey` is omitted, the runtime can resolve provider credentials from * tenant secrets based on the active tenant context. */ ai?: AgentAIOptions; /** * Suppress all log output (useful for CLI --json mode) * When true, creates a no-op logger that discards all messages */ silent?: boolean; /** * Opt into process-level SIGTERM/SIGINT handling for this instance. * * Host runtimes should generally own process lifecycle; this remains available * for single-agent CLIs and scripts that explicitly want it. Do not enable * this for multiple agents in the same process unless the host coordinates * shutdown itself; the first handler to finish exits the process. */ manageProcessSignals?: boolean; /** * Durable per-instance key for multi-instance agents (#1890). * * Only honored when the agent class opts into multi-instance * (`static multiInstance = true`); a singleton agent (the default) ignores it, * so passing a key can never change a non-opted agent's behavior. When honored * it becomes the per-instance dispatch subscriber suffix and memory partition * (see {@link Agent.getDispatchSubscriber} / {@link Agent.learningScope}) so N * instances of one class run independently. Typically the persona id from * `@happyvertical/smrt-personas` (a persona is a durable instance). */ instanceKey?: string | null; /** * Durable persona row that owns this agent's editable settings. * * This is deliberately independent from `instanceKey`: the reserved default * persona keeps the singleton runtime identity (`instanceKey: null`) but must * still load and save its own persona-scoped settings. */ personaId?: string | null; } /** * Base Agent class for building autonomous actors in the SMRT ecosystem * * Agents are SmrtObjects that perform specific tasks with: * - Status tracking (idle, initializing, running, error, shutdown) * - Configuration management via @have/config * - Structured logging via @happyvertical/logger * - Lifecycle hooks (initialize, validate, run, shutdown) * - Optional process signal handling for graceful shutdown * * Agents can define their own properties for state management - since they extend * SmrtObject, any properties defined will be automatically persisted to the database. * * **Important**: Extending classes must add the `@smrt()` decorator themselves * to configure CLI/API/MCP exposure. * * @example * ```typescript * import { Agent } from '@have/agents'; * import { getModuleConfig } from '@have/config'; * import { smrt } from '@happyvertical/smrt-core'; * * @smrt() * class MyAgent extends Agent { * protected config = getModuleConfig('my-agent', { * cronSchedule: '0 2 * * *', * maxRetries: 3 * }); * * // Define your own state properties (automatically persisted) * lastCrawl: Date | null = null; * itemsProcessed: number = 0; * * async validate(): Promise { * if (!this.config.cronSchedule) { * throw new Error('cronSchedule is required'); * } * } * * async run(): Promise { * // Agent logic here * this.itemsProcessed = 42; * this.lastCrawl = new Date(); * await this.save(); // Persist state * } * } * * const agent = new MyAgent({ name: 'my-agent' }); * await agent.execute(); * ``` */ export declare abstract class Agent extends SmrtObject { /** * Tenant ID for multi-tenant isolation * Nullable to support both tenant-scoped and global agents */ tenantId: string | null; /** * UI slots this agent supports for admin panels * * Subclasses override this to declare their admin UI slots. * Each slot can be implemented by a Svelte component. * * @example * ```typescript * static override uiSlots: AgentUISlots = { * sources: { * id: 'sources', * label: 'News Sources', * description: 'Configure scrapers and data sources', * icon: 'database', * order: 1, * }, * settings: { * id: 'settings', * label: 'Agent Settings', * description: 'Configure agent behavior', * icon: 'settings', * order: 2, * }, * }; * ``` */ static uiSlots: AgentUISlots; /** * Admin routes this agent provides * * Subclasses override this to declare admin route metadata. * The vitePluginAgentRoutes Vite plugin reads these from the manifest * and registers them so host applications can discover and render them. * * @example * ```typescript * static override adminRoutes: AgentAdminRoute[] = [ * { path: 'sources', component: 'SourcesPanel', load: 'loadSources' }, * { path: 'sources/[sourceId]', component: 'SourceDetail', load: 'loadSourceDetail' }, * ]; * ``` */ static adminRoutes: AgentAdminRoute[]; /** * Signal types this agent subscribes to by default * * These are seedable defaults — on `initialize()`, the agent checks the * database first and only creates subscriptions that don't already exist. * The database is the runtime source of truth, allowing users to customize * subscriptions per-tenant via the dashboard without code changes. * * When declared, `execute()` will automatically call `processDispatches()` * before `run()`, so handler agents don't need to manually poll. * Override `handleDispatch()` to process incoming dispatches. * * @example * ```typescript * @smrt({ agent: { icon: 'mail', tier: 'standard' } }) * class EmailHandler extends Agent { * static override signalSubscriptions = ['email.received', 'email.bounced']; * * async handleDispatch(payload: unknown, metadata: DispatchMetadata) { * // Called automatically during execute() for each pending dispatch * } * * async run() { ... } * } * ``` */ static signalSubscriptions: string[]; /** * Execute-time resolvers for `agent_config` fields that should be computed * lazily rather than snapshotted at sync time. * * Each entry is keyed by the agent_config field it produces. The runtime * (see {@link resolveLazyConfig}) calls every resolver and overlays the * results on top of the persisted config before constructing the agent. * That means env-derived values like asset storage paths, S3 buckets, AI * provider keys, or tenant-scoped DB URLs stay live: rotating an env var * is reflected on the next scheduled run without rewriting the schedule * row. * * Resolvers may be sync or async. Returning `undefined` or `null` leaves * the persisted value in place — both are treated as "no overlay" so the * common `() => process.env.X ?? null` pattern is safe and won't clobber * a snapshotted value when the env var is unset. Throwing falls back to * the persisted value (or to whatever * {@link ResolveLazyConfigOptions.onError} dictates). * * @example * ```typescript * class Praeco extends Agent { * static override configResolvers = { * assetStorage: () => resolveSharedAssetStorage(), * aiKey: async () => loadAIKeyFromSecretsManager(), * }; * } * ``` */ static configResolvers: Record; /** * Opt-in learning trait declaration (#1886). * * **Off by default.** Set to `true` (or a config object) on a subclass to * wire a confidence-scored recall-before / capture-after loop into the agent * lifecycle, backed by {@link LearningMemory}. A non-opted agent behaves * byte-for-byte as it does today — the learning branches are never entered. * * When enabled, the loop wraps `run()` itself (in {@link initialize}), so it * fires whether the agent runs via {@link execute} or the background/scheduled * path (which calls `run()` directly). Each run: * 1. recalls confident memories for {@link learningScope} before `run()`, * exposing them via {@link recalledMemories}; * 2. captures the run outcome after `run()` — a clean completion reinforces * the staged memory (see {@link stageLearning}); a thrown error or an * explicit {@link reportLearningOutcome} failure decays it. * * @example * ```typescript * @smrt() * class InvoiceAgent extends Agent { * static override learning = true; // reuse floor 0.7, success 0.9, fail 0.3 * // or: static override learning = { minConfidence: 0.8, scope: 'invoices' }; * protected config = {}; * async run() { * const [cached] = this.recalledMemories; * const strategy = cached?.value ?? (await this.generateStrategy()); * this.stageLearning({ scope: this.learningScope(), key: 'default', value: strategy }); * } * } * ``` */ static learning: AgentLearningDeclaration; /** * Opt into multiple durable instances of this agent class per tenant (#1890). * * **Off by default** — a non-opted class is a **singleton** (the N=1 case) and * behaves byte-for-byte as it does today: one dispatch subscriber keyed by the * agent type, one memory scope, class-wide interests. Setting this to `true` * lets N configured instances (personas, from `@happyvertical/smrt-personas`) * run independently: each is constructed with its own {@link AgentOptions.instanceKey}, * which the framework folds into a per-instance dispatch subscriber * ({@link getDispatchSubscriber}), memory partition ({@link learningScope}), * and interest/subscription scoping seams ({@link instanceInterestFilter} / * {@link resolveSignalSubscriptions}) so two instances never double-process * each other's dispatches or interests. * * The framework provides the per-instance *identity*; a package scopes its own * dispatch/interests to the instance's config by overriding the seams. The * `default` persona reuses the singleton identity (null key), which makes the * singleton→multi upgrade non-destructive. */ static multiInstance: boolean; /** * Current agent status */ status: AgentStatusType; /** * Structured logger instance * Created with agent's class name as context */ protected logger: Logger; /** * Agent configuration * Must be defined by extending classes using getModuleConfig() * * @example * ```typescript * protected config = getModuleConfig('my-agent', { * cronSchedule: '0 0 * * *', * maxRetries: 3 * }); * ``` */ protected abstract config: unknown; /** * Signal handlers for graceful shutdown */ private signalHandlers; /** * Cached DispatchBus instance for inter-agent communication */ private _dispatch; /** * Cached LearningMemory binding, once successfully built. Not cached when * learning is disabled or the DB isn't ready yet, so an early call can't * permanently stick the agent in a learning-disabled state. */ private _learningMemory?; /** * Whether `run()` has been wrapped with the learning loop (idempotency guard). */ private _runWrappedForLearning; /** * The episode the current run acted on, staged via {@link stageLearning} so * the lifecycle can reinforce it after `run()`. */ private _learningEpisode; /** * Explicit outcome for the current run, set via * {@link reportLearningOutcome}. When unset, a clean `run()` is treated as a * success and a thrown error as a failure. */ private _learningOutcome; /** * Memories recalled before `run()` when the learning trait is enabled. * * Empty for non-opted agents. Populated by the lifecycle (see * {@link recallForRun}); read from `run()` to reuse prior knowledge. */ protected recalledMemories: LearningMemoryRecord[]; /** * Creates a new Agent instance * * @param options - Configuration options including identifiers and metadata */ constructor(options?: AgentOptions); /** * Interest configuration for this agent * Lazily accessed from options on first interesting() call */ protected get interests(): InterestOptions | undefined; /** * Canonical agent type for persistence and dispatch routing. */ protected getAgentTypeName(): string; /** * Human-readable class name for logs and UI. */ protected getAgentClassName(): string; /** * Whether this agent class opted into multiple durable instances per tenant. */ protected isMultiInstance(): boolean; /** * The durable per-instance key for this agent, or `null` for a singleton. * * Returns `null` unless the class opts in (`static multiInstance = true`) AND a * non-empty {@link AgentOptions.instanceKey} was supplied — so a non-opted * agent is always singleton-identified even if a key is passed. This is the * anchor the framework folds into the dispatch subscriber, memory scope, and * scoping seams below. */ getInstanceKey(): string | null; /** * Durable owner id used for database-backed slot configuration. * * Persona-backed agents use the persona row id, including the default * persona whose runtime instance key remains null. Legacy/singleton agents * continue to use their persisted Agent STI row id. */ getConfigOwnerId(slotId?: string): string | null; /** * Canonical dispatch subscriber identity for this agent. * * A singleton (no instance key) is the bare agent type — **unchanged** from the * class-keyed behavior. A multi-instance agent is `` `${agentType}#${key}` ``, * giving each instance its own subscription rows and its own pending-dispatch * queue so instances don't compete for or double-process each other's * dispatches. Used everywhere the agent subscribes, seeds, and processes. */ getDispatchSubscriber(): string; /** * The signal types this instance should seed as dispatch subscriptions. * * Defaults to the class's static {@link Agent.signalSubscriptions} unchanged. * A multi-instance package overrides this to derive **instance-scoped** signal * types from the persona/instance config (e.g. append the instance key or a * routing dimension), so an emit meant for one instance only matches that * instance's subscription and the other never processes it. */ protected resolveSignalSubscriptions(): string[]; /** * An optional filter AND-merged (as the base layer) into every * {@link interesting} query for this instance. * * `undefined` by default (no scoping — singleton behavior unchanged). A * multi-instance package overrides it to return an instance-discriminating * filter derived from the persona/instance config, so two instances of one * class partition the objects they process and never double-handle the same * row. Global and per-object interest filters layer on top (and win on key * collision), so choose a dedicated discriminator key here. * * Applies to the standard filter path; custom `query` interest filters own * their SQL and should incorporate {@link getInstanceKey} themselves. */ protected instanceInterestFilter(): ObjectFilter | undefined; /** * Get UI slot definitions for this agent instance * * Returns the static uiSlots defined on the agent's class. * Used by host applications to discover available admin panels. * * @example * ```typescript * const slots = agent.getUISlots(); * for (const [slotId, slot] of Object.entries(slots)) { * console.log(`${slot.label}: ${slot.description}`); * } * ``` */ getUISlots(): AgentUISlots; /** * Load all database-persisted configs for this agent * * Returns a Map of slotId → configData for all saved configurations. * Use getMergedConfig() to get file + db merged config for a slot. * * @returns Map of slotId to config data * * @example * ```typescript * const configs = await agent.loadConfigs(); * const sources = configs.get('sources'); * ``` */ loadConfigs(): Promise>>; /** * Save config for a specific UI slot to the database * * Persists configuration data that can be modified by admin panels. * Use this when the user saves changes in an admin UI. * * @param slotId - The UI slot ID (e.g., 'sources', 'settings') * @param data - Configuration data to save * * @example * ```typescript * await agent.saveSlotConfig('sources', { * scrapers: ['civicweb', 'govstack'], * refreshInterval: 3600 * }); * ``` */ saveSlotConfig(slotId: string, data: Record): Promise; /** * Get merged config for a slot (file-based + database) * * Priority order (highest to lowest): * 1. Database-persisted config (from saveSlotConfig) * 2. File-based config (from getModuleConfig) * 3. Agent class defaults * * @param slotId - The UI slot ID * @returns Merged configuration object * * @example * ```typescript * const sourcesConfig = await agent.getMergedConfig('sources'); * // Returns file config merged with any db overrides * ``` */ getMergedConfig(slotId: string): Promise>; /** * Export all config for this agent (for static site generation) * * Merges file-based and database configs, then optionally sanitizes * to remove secrets. Use this before building a static site. * * @param options - Export options * @param options.includeSecrets - If true, includes API keys and secrets (default: false) * @returns Merged configuration object * * @example * ```typescript * // Export for static build (secrets filtered) * const config = await agent.exportConfig(); * * // Export with secrets (for secure environments) * const fullConfig = await agent.exportConfig({ includeSecrets: true }); * ``` */ exportConfig(options?: { includeSecrets?: boolean; }): Promise>; /** * Get the DispatchBus for inter-agent communication * * Creates a DispatchBus lazily on first access. Requires database configuration. * * @example * ```typescript * // Emit a dispatch to other agents * await this.dispatch.emit('campaign.completed', { * campaignId: '123', * revenue: 5000 * }, { source: this.constructor.name }); * * // Subscribe to dispatches * await this.dispatch.subscribe({ * signalType: 'campaign.*', * subscriber: this.constructor.name * }); * ``` * * @throws Error if database is not configured */ getDispatch(): Promise; /** * Handle incoming dispatches * * Override this method to process dispatches targeted at this agent. * Called when process() is invoked for this agent's subscriber name. * * @param payload - Dispatch payload data * @param metadata - Dispatch metadata including type, source, and timing * * @example * ```typescript * async handleDispatch(payload: unknown, metadata: DispatchMetadata): Promise { * if (metadata.type === 'campaign.completed') { * const data = payload as { campaignId: string; revenue: number }; * await this.recordRevenue(data.campaignId, data.revenue); * } * } * ``` */ handleDispatch(_payload: unknown, _metadata: DispatchMetadata): Promise; /** * Process pending dispatches for this agent * * Finds and processes all pending dispatches that match this agent's subscriptions. * Uses handleDispatch() to process each dispatch. * * @returns Number of dispatches processed * * @example * ```typescript * // In your run() method * const processed = await this.processDispatches(); * this.logger.info(`Processed ${processed} dispatches`); * ``` */ processDispatches(): Promise; /** * Base memory scope for this agent's learning. * * Defaults to the configured `scope` (if any) or `agent/`. * Override to shape how memories are filed (e.g. per task type). Recall and * capture are additionally isolated by the agent instance id (owner), so * memory never bleeds across tenants running the same agent class. */ protected learningScope(): string; /** * Optional semantic-search arm for {@link LearningMemory}. * * Returns `undefined` by default (keyed-context recall only). Override to * wire embedding search — e.g. return a bound `collection.semanticSearch`. */ protected getLearningSemanticSearch(): LearningSemanticSearch | undefined; /** * Resolve the tenant id used for the learning scope and semantic filtering. */ private resolveLearningTenantId; /** * Get this agent's {@link LearningMemory} binding, or `null` when learning is * disabled or no database is configured. * * Cheap and side-effect-free when the trait is off (returns `null` after a * single static-flag check), which keeps non-opted agents unchanged. */ getLearningMemory(): LearningMemory | null; /** * Wrap `run()` with the recall-before / capture-after learning loop when the * trait is enabled, so it fires **however run() is invoked** — via * {@link execute} OR directly by the background/scheduled path * (`ScheduleRunner` → `TaskRunner` calls the agent's configured method, which * defaults to `run` and never goes through `execute()`). Both paths call * {@link initialize}, so wrapping here covers them. Idempotent, and a no-op * for non-opted agents (their `run()` is left untouched). */ private wrapRunForLearning; /** * Recall relevant memories before `run()`. * * Default: a scope-wide, confidence-filtered recall of {@link learningScope}. * Override to shape the recall (e.g. a keyed lookup or a semantic query). */ protected recallForRun(memory: LearningMemory): Promise; /** * Capture the run outcome after `run()`. * * Default: reinforce the memory staged via {@link stageLearning}. A no-op * when nothing was staged. Override for bespoke capture logic. */ protected captureForRun(memory: LearningMemory, outcome: LearningOutcome): Promise; /** * Stage the memory episode the current run acted on, so the lifecycle * reinforces it after `run()` completes. Call from `run()`. */ protected stageLearning(episode: LearningEpisode): void; /** * Report an explicit outcome for the current run (e.g. a validated failure * that did not throw). Overrides the default success/throw inference. */ protected reportLearningOutcome(outcome: LearningOutcome): void; /** * Initialize the agent * Sets status to 'initializing' and sets up signal handlers * * Override to perform setup after construction, but always call super.initialize() * * @example * ```typescript * async initialize(): Promise { * await super.initialize(); * // Custom initialization logic * } * ``` */ initialize(): Promise; /** * Set up signal handlers for graceful shutdown * Handles SIGTERM and SIGINT for single-agent processes that explicitly opt in. */ private setupSignalHandlers; /** * Migrate legacy simple-name dispatch subscribers to the canonical agent type. * * Older releases used `this.constructor.name` directly for subscriber IDs. * That collides across packages and leaves fan-out dispatches targeted at the * wrong subscriber once qualified names are available. */ private migrateLegacyDispatchSubscriptions; /** * Clean up signal handlers */ private cleanupSignalHandlers; /** * Validate configuration and dependencies * Override to check agent-specific requirements * * @throws Error if validation fails * * @example * ```typescript * async validate(): Promise { * if (!this.config.apiKey) { * throw new Error('API key is required'); * } * } * ``` */ validate(): Promise; /** * Main agent logic * Must be implemented by extending class * * Update this.lastRun.itemsProcessed to track work done * * @example * ```typescript * async run(): Promise { * this.logger.info('Starting agent work'); * let processed = 0; * * for (const item of items) { * await this.processItem(item); * processed++; * } * * this.lastRun.itemsProcessed = processed; * this.logger.info(`Processed ${processed} items`); * } * ``` */ abstract run(): Promise; /** * Cleanup and shutdown * Override to perform graceful shutdown * * Always call super.shutdown() to clean up signal handlers * * @example * ```typescript * async shutdown(): Promise { * this.logger.info('Cleaning up resources'); * await this.cleanup(); * await super.shutdown(); * } * ``` */ shutdown(): Promise; /** * Execute agent with lifecycle management * * Runs the full lifecycle: * 1. initialize() — seeds signal subscriptions if declared * 2. validate() * 3. processDispatches() — auto-processes pending dispatches if subscriptions exist * 4. run() * * Note: handleDispatch() callbacks may fire before run() is entered. * * On error: * 1. Sets status to 'error' * 2. Logs error * 3. Re-throws error * * @example * ```typescript * const agent = new MyAgent({ name: 'my-agent' }); * * try { * await agent.execute(); * console.log('Agent completed successfully'); * } catch (error) { * console.error('Agent failed:', error); * } * ``` */ execute(): Promise; /** * Query objects this agent is interested in * * Returns items from all configured object types, filtered and sorted * according to interest configuration. If handlers are defined on filters, * they are called for each matched item and the result is included. * * @returns Array of { type, data, name?, handled? } results * @throws Error if no interests are configured * * @example * ```typescript * const items = await this.interesting(); * for (const { type, data, name, handled } of items) { * console.log(`Processing ${type} from "${name}": action=${handled?.action}`); * } * ``` */ interesting(): Promise; /** * Query a single object type based on interest config * * Supports both single filter and array of filters. * Each filter can use standard SDK filters OR custom query function. * Returns InterestResult[] with handler results included. */ private queryInterestingObjects; /** * Normalize ObjectInterestConfig to array format */ private normalizeInterestConfig; /** * Query a single interest filter * * Uses collection.query() for custom query functions, * or collection.list() for standard SDK filters. */ private queryInterestFilter; /** * Sort results by field(s) across all types */ private sortResults; } //# sourceMappingURL=agent.d.ts.map