@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)
| Export | Description |
|---|---|
Agent | Base agent class with lifecycle and interests |
AgentOptions | Constructor options type |
AgentStatusType | Status enum: idle/initializing/running/error/shutdown |
AgentConfig | DB-persisted agent configuration model |
AgentConfigCollection | Collection for AgentConfig |
AgentSchedule | Cron-based schedule model (_smrt_agent_schedules) |
AgentScheduleCollection | Collection for AgentSchedule |
ScheduleStatus | Schedule status type |
TenantAgent | Agent-to-tenant junction with hierarchy resolution |
TenantAgentCollection | Collection for TenantAgent |
TenantAgentStatus | Tenant agent status type |
ResolvedAgentAvailability | Resolved availability after hierarchy walk |
mergeFilters | Combine interest filters |
normalizeSort | Normalize sort expressions |
InterestOptions | Interest configuration type |
InterestFilter | Filter definition type |
InterestResult | Discovery result type |
ObjectInterestConfig | Per-object interest config type |
ObjectFilter | Object filter type |
InterestHandlerFn | Interest handler function type |
AsyncQualifierFn | Async post-filter qualifier type |
QueryFn | Query function type |
AgentWithInterestsOptions | Agent options with interests |
createReportDataSurfaceTools | Principal-bound report discovery, query, lifecycle, drilldown, and export tools |
ReportDataSurfaceToolsOptions | Server-owned report catalog and application-host seams |
createSmrtCollectionDataSurfaceDefinition | Generic registry-driven SmrtObject collection → DataSurfaceDefinition adapter, with field-policy redaction and offset/cursor paging |
buildDataQuerySchemaForClass | Memoized DataQuerySchema derived from ObjectRegistry field metadata for an arbitrary registered class |
executeSmrtCollectionQuery | Bounded DataQueryRequest execution against any list/count/facets-shaped collection |
CreateSmrtCollectionDataSurfaceOptions | Options for createSmrtCollectionDataSurfaceDefinition |
SmrtCollectionQueryCollection | Structural collection interface the generic adapter executes against |
SmrtCollectionQueryScope | Trusted, server-derived scope conditions for the generic adapter |
SmrtCollectionDataSurfaceAction | Declarative 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)
| Export | Description |
|---|---|
AgentUIRegistry | Singleton registry for agent admin panels |
createUIRegistry | Factory for UI registries |
AgentUISlot | UI slot definition type |
AgentUISlots | Map of UI slots |
AgentSettingsSchema | Versioned fallback settings form contract |
AgentAdminRoute | Admin route metadata (path, component, load) |
AgentAdminExport | Agent admin module export shape |
AgentAdminNavItem | Navigation item for admin sidebar |
AgentAdminRootProps | Props for admin root component |
AdminPanelBaseProps | Props for admin panel components |
AgentManifestInfo | Agent manifest metadata |
AgentRouteLoadContext | Normalized SvelteKit load context |
AgentRouteLoadFn | Server load function type |
AgentUIComponentRegistry | Component registry type |
ComponentType | Generic component type |
Vite Export (@happyvertical/smrt-agents/vite)
| Export | Description |
|---|---|
vitePluginAgentRoutes | Vite plugin for virtual:smrt-agent-registrations |
AgentRoutesPluginOptions | Plugin 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./sveltecomponents, including the agent-admin shells (AgentAdminPanel,AgentAdminTabs,AgentSettingsShell) that moved here from smrt-svelte in #1589- Peer (optional):
svelte