Skip to main content

@happyvertical/smrt-agents

Agent framework for building autonomous actors with lifecycle management, inter-agent messaging, interest-based discovery, scheduling, and multi-tenant bindings.

Installation

pnpm add @happyvertical/smrt-agents

Usage

import { Agent, type AgentOptions } from '@happyvertical/smrt-agents';
import { smrt } from '@happyvertical/smrt-core';
import { getModuleConfig } from '@happyvertical/smrt-config';

@smrt()
class MyAgent extends Agent {
protected config = getModuleConfig('my-agent', {
cronSchedule: '0 2 * * *',
maxRetries: 3,
});

itemsProcessed: number = 0;

constructor(options: AgentOptions = {}) {
super({
...options,
interests: {
objects: {
Meeting: { filter: { status: 'upcoming' } },
Document: { filter: { 'type in': ['agenda', 'minutes'] } },
},
},
});
}

async validate(): Promise<void> {
if (!this.config.cronSchedule) throw new Error('cronSchedule required');
}

async run(): Promise<void> {
const items = await this.interesting();
for (const { type, data } of items) {
this.logger.info(`Processing ${type}: ${data.id}`);
}
this.itemsProcessed = items.length;
await this.save();
}
}

const agent = new MyAgent({ name: 'my-agent' });
await agent.execute(); // initialize() -> validate() -> run() -> shutdown()

If you are running a single-agent CLI or script and want built-in SIGTERM/SIGINT handling, pass manageProcessSignals: true to the constructor. Multi-agent hosts should leave that off and coordinate process shutdown themselves.

Scheduled Methods Vs Operator Actions

Use scheduled agent methods for the regular, repeatable maintenance path: the work that should happen automatically every time the schedule fires. Keep those methods idempotent and safe to rerun.

When operators need a composite catch-up or repair flow, expose that as an explicit method such as forage(), backfill(), or rebuildIndex() instead of overloading run() with manual-only behavior. Those operator actions can still be enqueued through @happyvertical/smrt-jobs, but they should remain distinct from the normal scheduled loop so it stays obvious which work is automatic and which work is an intentional intervention.

Schema-driven settings

Agent packages can declare portable, non-secret settings without shipping a custom admin component. The manifest preserves the versioned schema and the agent admin shell renders AgentSettingsForm as a fallback.

static override uiSlots = {
notifications: {
id: 'notifications',
label: 'Notifications',
scope: 'persona',
settingsSchema: {
version: 1,
fields: [
{
id: 'minimumSeverity',
label: 'Minimum severity',
type: 'select',
options: [
{ value: 'info', label: 'Info' },
{ value: 'urgent', label: 'Urgent only' },
],
},
],
},
},
} satisfies AgentUISlots;

scope: 'persona' persists under AgentOptions.personaId; scope: 'agent' uses a saved Agent row id. An omitted scope chooses the persona when present and otherwise preserves legacy Agent-row behavior. Credentials should not be modeled as settings fields—use a package-specific write-only server boundary such as MessagingSettingsService.

API

Main Export (@happyvertical/smrt-agents)

ExportDescription
AgentBase agent class with lifecycle and interests
AgentOptionsConstructor options type
AgentStatusTypeStatus enum: idle/initializing/running/error/shutdown
AgentConfigDB-persisted agent configuration model
AgentConfigCollectionCollection for AgentConfig
AgentScheduleCron-based schedule model (_smrt_agent_schedules)
AgentScheduleCollectionCollection for AgentSchedule
ScheduleStatusSchedule status type
TenantAgentAgent-to-tenant junction with hierarchy resolution
TenantAgentCollectionCollection for TenantAgent
TenantAgentStatusTenant agent status type
ResolvedAgentAvailabilityResolved availability after hierarchy walk
mergeFiltersCombine interest filters
normalizeSortNormalize sort expressions
InterestOptionsInterest configuration type
InterestFilterFilter definition type
InterestResultDiscovery result type
ObjectInterestConfigPer-object interest config type
ObjectFilterObject filter type
InterestHandlerFnInterest handler function type
AsyncQualifierFnAsync post-filter qualifier type
QueryFnQuery function type
AgentWithInterestsOptionsAgent options with interests
createReportDataSurfaceToolsPrincipal-bound report discovery, query, lifecycle, drilldown, and export tools
ReportDataSurfaceToolsOptionsServer-owned report catalog and application-host seams
createSmrtCollectionDataSurfaceDefinitionGeneric registry-driven SmrtObject collection → DataSurfaceDefinition adapter, with field-policy redaction and offset/cursor paging
buildDataQuerySchemaForClassMemoized DataQuerySchema derived from ObjectRegistry field metadata for an arbitrary registered class
executeSmrtCollectionQueryBounded DataQueryRequest execution against any list/count/facets-shaped collection
CreateSmrtCollectionDataSurfaceOptionsOptions for createSmrtCollectionDataSurfaceDefinition
SmrtCollectionQueryCollectionStructural collection interface the generic adapter executes against
SmrtCollectionQueryScopeTrusted, server-derived scope conditions for the generic adapter
SmrtCollectionDataSurfaceActionDeclarative row/bulk action catalog entry surfaced via metadata.actionCatalog

Report Data-Surface Tools

createReportDataSurfaceTools() combines the generic read-only data-surface tools with reports.query, reports.refresh, reports.drilldown, and reports.export. Configure report constructors and all transport/action seams on the server. Queries inherit authority exclusively from the live PrincipalRun; visible commands require an exact browser acknowledgement, and refresh/export retain application-owned authorization and audit hosts. Discovery preserves each visible report field's kind, WHERE/HAVING filter scope, and capabilities, plus the currently available report actions. When an authenticated database is present, silent and visible reads return the tenant-safe lifecycle-derived freshness state (including stale or lock-skipped materializations). Advertised actions are filtered by the live principal's tool allow-list and effective permissions; action hosts still reauthorize before mutation.

Generic SmrtObject Collection Data-Surface

createSmrtCollectionDataSurfaceDefinition() builds a DataSurfaceDefinition for an arbitrary registered SmrtObject collection (events, ad zones, schedules, social accounts, meetings, tasks, …), not just Content:

const definition = await createSmrtCollectionDataSurfaceDefinition({
qualifiedName: '@myapp/events:Event',
collectionName: 'events', // permission-catalog collection
collection: async ({ principal }) => getEventsCollection(principal.tenantId),
scope: ({ principal }) => ({ tenantId: principal.tenantId }),
actions: [{ id: 'cancel', label: 'Cancel', requiresConfirmation: true }],
});

The schema is derived from ObjectRegistry.getAllFields(): sensitive and readPermission-gated fields, transient/non-column-backed fields, the class's configured tenant field (from @TenantScoped(); unscoped classes get no tenant field exclusion or scoping at all), and internal _-prefixed fields are never declared — the same field-policy boundary @happyvertical/smrt-content's ContentList adapter enforces. A host-supplied schema override is intersected with this same registry-derived exclusion set, so it can only narrow, never widen, what is advertised. Execution ANDs the tenant read scope and the application scope into every branch of the caller's filter (all/any/not/condition, lowered to bounded disjunctive-normal-form where conditions), supports both offset and opaque-cursor paging, and never hydrates the full collection. An explicit, normalized-empty application scope denies all rows and short-circuits without calling the collection.

Cursors are opaque and bound to the exact query: they encode { binding, offset }, where binding fingerprints the normalized request (filter, sort, projection) plus the merged tenant/application scope, so a cursor from a different query, filter, sort, or tenant is rejected rather than silently misapplied. facets support defaults from the collection's shape (typeof collection.facets === 'function' for a static collection; true for a resolver-backed one, which must set facets: false if its resolved collection lacks facets()). Because SmrtCollectionQueryCollection is structural, the adapter cannot read a host collection's own row-limit cap — set maxPageLimit at or below that cap, or a clamped page is detected and reported via a result warning rather than corrected. See src/smrt-collection-data-surface.ts for the full contract and docs/data-surface-conformance.md for the integration checklist.

Server Export (@happyvertical/smrt-agents/server)

createDataSurfaceActionAdapter() provides server-only preview and confirmed apply orchestration for the smrt-ui data-surface action contract. Browser selections and action payloads are hints, never authority: each action declares its input validator, confirmation policy, principal tool/RBAC operation, fresh authorization and row-eligibility checks, and foreground or injected-background execution.

Preview issues a short-lived opaque token bound to the principal, tenant, surface/action, selection, query fingerprint, and revision. Apply verifies that binding for confirmation-required actions and repeats its principal-bound checks before returning accepted, skipped, and failed row outcomes. Actions declared with confirmation: 'none' may apply directly with an idempotency key; every other apply must include its current preview token. Callers must supply a durable shared DataSurfaceActionStateStore with atomic token and idempotency operations. createSqlDataSurfaceActionStateStore() uses the application's migrated s-m-r-t database; token consumption and apply reservation commit together, while preview tokens and reservation owner nonces are stored as hashes. An orphaned reservation is never expired or retried automatically because its external effects may be unknown. A host may only reconcile it to a terminal result through reconcileIdempotency() after the configured live-authority callback accepts evidence for the exact request fingerprint and reservation timestamp. InMemoryDataSurfaceActionStateStore is for single-process test harnesses only.

createJobsDataSurfaceBackgroundQueue() persists a versioned envelope in @happyvertical/smrt-jobs. The envelope contains the request and a non-secret principal reference, never permission snapshots or confirmation-token secrets. Register the same stable handler ID in every worker process and configure the adapter with that backgroundHandlerId and a server-only deferredEnvelopeSigningKey of at least 32 bytes. The key authenticates every persisted envelope field and is never added to the job payload. Worker delivery calls adapter.executeDeferred(), which resolves current principal authority again before mutation. The original job.run() callback remains available for in-process queue adapters.

createDataSurfaceActionRouteHandlers() wires a SvelteKit (or any Web Request/Response) route to the adapter: it resolves the principal per request, parses the body, and maps every refusal reason to an HTTP status. resolveBulkExplicitIds() expands a browser-supplied anchor row id into a full bulk selection server-side (e.g. "every reference photo for this performer") so one idempotency key covers the whole set. See docs/data-surface-sveltekit-wiring.md for the full route → principal → action → queue wiring guide, including the refusal-to-status table.

UI Export (@happyvertical/smrt-agents/ui)

ExportDescription
AgentUIRegistrySingleton registry for agent admin panels
createUIRegistryFactory for UI registries
AgentUISlotUI slot definition type
AgentUISlotsMap of UI slots
AgentSettingsSchemaVersioned fallback settings form contract
AgentAdminRouteAdmin route metadata (path, component, load)
AgentAdminExportAgent admin module export shape
AgentAdminNavItemNavigation item for admin sidebar
AgentAdminRootPropsProps for admin root component
AdminPanelBasePropsProps for admin panel components
AgentManifestInfoAgent manifest metadata
AgentRouteLoadContextNormalized SvelteKit load context
AgentRouteLoadFnServer load function type
AgentUIComponentRegistryComponent registry type
ComponentTypeGeneric component type

Vite Export (@happyvertical/smrt-agents/vite)

ExportDescription
vitePluginAgentRoutesVite plugin for virtual:smrt-agent-registrations
AgentRoutesPluginOptionsPlugin options type

Dependencies

  • @happyvertical/smrt-core -- ORM base classes
  • @happyvertical/smrt-config -- Configuration management
  • @happyvertical/smrt-tenancy -- Multi-tenant context
  • @happyvertical/ai -- AI client (SDK)
  • @happyvertical/files -- Filesystem utilities (SDK)
  • @happyvertical/utils -- Shared utilities (SDK)
  • @happyvertical/smrt-ui -- UI runtime (i18n client, primitives, module registry) for the optional ./svelte components, including the agent-admin shells (AgentAdminPanel, AgentAdminTabs, AgentSettingsShell) that moved here from smrt-svelte in #1589
  • Peer (optional): svelte