@happyvertical/smrt-agents 0.37.2 → 0.37.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,525 +1,449 @@
1
- import { ObjectRegistry, smrt, SmrtObject, createDispatchBus, resolveDispatchTenantScope, field, SmrtCollection } from "@happyvertical/smrt-core";
2
- import { getClassConfigResolvers, getConfigResolver, isLazyConfigSentinel, listConfigResolvers, registerConfigResolver, resetConfigResolvers, resolveLazyConfig, unregisterConfigResolver } from "@happyvertical/smrt-core";
1
+ import { a as getAgentTypeName, i as getAgentTypeAliases, n as AgentConfigCollection, r as getAgentClassName, t as AgentConfig } from "./chunks/config-C73buv9Y.js";
2
+ import { AgentUIRegistry, createUIRegistry } from "./ui.js";
3
+ import { ObjectRegistry, SmrtCollection, SmrtObject, createDispatchBus, field, getClassConfigResolvers, getConfigResolver, isLazyConfigSentinel, listConfigResolvers, registerConfigResolver, resetConfigResolvers, resolveDispatchTenantScope, resolveLazyConfig, smrt, unregisterConfigResolver } from "@happyvertical/smrt-core";
3
4
  import { createLogger } from "@happyvertical/logger";
4
5
  import { sanitizeConfig } from "@happyvertical/smrt-config";
5
- import { getCurrentTenant, withTenant, tenantId, TenantScoped, queryGlobal, queryWithGlobals } from "@happyvertical/smrt-tenancy";
6
+ import { TenantScoped, getCurrentTenant, queryGlobal, queryWithGlobals, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
6
7
  import { SecretService } from "@happyvertical/smrt-secrets";
7
8
  import { TenantCollection } from "@happyvertical/smrt-users";
8
- import { g as getAgentTypeName, a as getAgentClassName, A as AgentConfig, b as getAgentTypeAliases } from "./chunks/config-JYiYqNE-.js";
9
- import { c } from "./chunks/config-JYiYqNE-.js";
10
- import { AgentUIRegistry, createUIRegistry } from "./ui.js";
11
- ObjectRegistry.registerPackageManifest(
12
- new URL("./manifest.json", import.meta.url)
13
- );
14
- const DEFAULT_SECRET_NAMES = {
15
- anthropic: "ANTHROPIC_API_KEY",
16
- gemini: "GEMINI_API_KEY",
17
- openai: "OPENAI_API_KEY"
9
+ //#region src/__smrt-register__.ts
10
+ ObjectRegistry.registerPackageManifest(new URL("./manifest.json", "" + import.meta.url));
11
+ //#endregion
12
+ //#region src/ai-config.ts
13
+ var DEFAULT_SECRET_NAMES = {
14
+ anthropic: "ANTHROPIC_API_KEY",
15
+ gemini: "GEMINI_API_KEY",
16
+ openai: "OPENAI_API_KEY"
18
17
  };
19
- const DEFAULT_SECRET_FALLBACK = "ancestors";
20
- const secretServiceCache = /* @__PURE__ */ new WeakMap();
21
- const tenantCollectionCache = /* @__PURE__ */ new WeakMap();
18
+ var DEFAULT_SECRET_FALLBACK = "ancestors";
19
+ var secretServiceCache = /* @__PURE__ */ new WeakMap();
20
+ var tenantCollectionCache = /* @__PURE__ */ new WeakMap();
22
21
  function asNonEmptyString(value) {
23
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
22
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
24
23
  }
25
24
  function normalizeSecretFallback(value) {
26
- return value === "none" ? "none" : DEFAULT_SECRET_FALLBACK;
25
+ return value === "none" ? "none" : DEFAULT_SECRET_FALLBACK;
27
26
  }
28
27
  function getDefaultSecretName(aiConfig) {
29
- const provider = asNonEmptyString(aiConfig.type)?.toLowerCase();
30
- if (!provider) {
31
- return void 0;
32
- }
33
- return DEFAULT_SECRET_NAMES[provider];
28
+ const provider = asNonEmptyString(aiConfig.type)?.toLowerCase();
29
+ if (!provider) return;
30
+ return DEFAULT_SECRET_NAMES[provider];
34
31
  }
35
32
  function stripAgentAISecretFields(aiConfig) {
36
- const {
37
- apiKeySecretName: _apiKeySecretName,
38
- apiKeySecretFallback: _apiKeySecretFallback,
39
- ...rest
40
- } = aiConfig;
41
- return rest;
33
+ const { apiKeySecretName: _apiKeySecretName, apiKeySecretFallback: _apiKeySecretFallback, ...rest } = aiConfig;
34
+ return rest;
42
35
  }
43
36
  async function getSecretService(db) {
44
- const existing = secretServiceCache.get(db);
45
- if (existing) {
46
- return await existing;
47
- }
48
- const created = SecretService.create({ db });
49
- secretServiceCache.set(db, created);
50
- return await created;
37
+ const existing = secretServiceCache.get(db);
38
+ if (existing) return await existing;
39
+ const created = SecretService.create({ db });
40
+ secretServiceCache.set(db, created);
41
+ return await created;
51
42
  }
52
43
  async function getTenantCollection(db) {
53
- const existing = tenantCollectionCache.get(db);
54
- if (existing) {
55
- return await existing;
56
- }
57
- const created = TenantCollection.create({ db });
58
- tenantCollectionCache.set(db, created);
59
- return await created;
44
+ const existing = tenantCollectionCache.get(db);
45
+ if (existing) return await existing;
46
+ const created = TenantCollection.create({ db });
47
+ tenantCollectionCache.set(db, created);
48
+ return await created;
60
49
  }
61
- async function getTenantSearchOrder(db, tenantId2, fallback) {
62
- const tenantIds = [tenantId2];
63
- if (fallback !== "ancestors") {
64
- return tenantIds;
65
- }
66
- const tenants = await getTenantCollection(db);
67
- const ancestors = await tenants.getAncestors(tenantId2);
68
- for (const tenant of ancestors) {
69
- if (tenant.id) {
70
- tenantIds.push(tenant.id);
71
- }
72
- }
73
- return tenantIds;
50
+ async function getTenantSearchOrder(db, tenantId, fallback) {
51
+ const tenantIds = [tenantId];
52
+ if (fallback !== "ancestors") return tenantIds;
53
+ const ancestors = await (await getTenantCollection(db)).getAncestors(tenantId);
54
+ for (const tenant of ancestors) if (tenant.id) tenantIds.push(tenant.id);
55
+ return tenantIds;
74
56
  }
75
57
  async function resolveSecretValue(service, tenantIds, secretName) {
76
- for (const tenantId2 of tenantIds) {
77
- const value = await withTenant({ tenantId: tenantId2 }, async () => {
78
- try {
79
- return (await service.retrieve(secretName)).value;
80
- } catch (error) {
81
- if (isMissingSecretError(error, secretName)) {
82
- return void 0;
83
- }
84
- throw error;
85
- }
86
- });
87
- if (value) {
88
- return value;
89
- }
90
- }
91
- return void 0;
58
+ for (const tenantId of tenantIds) {
59
+ const value = await withTenant({ tenantId }, async () => {
60
+ try {
61
+ return (await service.retrieve(secretName)).value;
62
+ } catch (error) {
63
+ if (isMissingSecretError(error, secretName)) return;
64
+ throw error;
65
+ }
66
+ });
67
+ if (value) return value;
68
+ }
92
69
  }
93
70
  function isMissingSecretError(error, secretName) {
94
- if (!(error instanceof Error)) {
95
- return false;
96
- }
97
- return error.message === `Secret '${secretName}' not found` || error.message === "Secret not found";
71
+ if (!(error instanceof Error)) return false;
72
+ return error.message === `Secret '${secretName}' not found` || error.message === "Secret not found";
98
73
  }
99
74
  async function resolveAgentAIOptions(input) {
100
- const { aiConfig, db } = input;
101
- if (!aiConfig) {
102
- return void 0;
103
- }
104
- const normalized = { ...aiConfig };
105
- if (asNonEmptyString(normalized.apiKey)) {
106
- return stripAgentAISecretFields(normalized);
107
- }
108
- const secretName = asNonEmptyString(normalized.apiKeySecretName) ?? getDefaultSecretName(normalized);
109
- if (!secretName || !db) {
110
- return stripAgentAISecretFields(normalized);
111
- }
112
- const tenantId2 = asNonEmptyString(input.tenantId) ?? asNonEmptyString(getCurrentTenant()?.tenantId);
113
- if (!tenantId2) {
114
- return stripAgentAISecretFields(normalized);
115
- }
116
- const fallback = normalizeSecretFallback(normalized.apiKeySecretFallback);
117
- const tenantIds = await getTenantSearchOrder(db, tenantId2, fallback);
118
- const service = await getSecretService(db);
119
- const apiKey = await resolveSecretValue(service, tenantIds, secretName);
120
- if (!apiKey) {
121
- return stripAgentAISecretFields(normalized);
122
- }
123
- return {
124
- ...stripAgentAISecretFields(normalized),
125
- apiKey
126
- };
75
+ const { aiConfig, db } = input;
76
+ if (!aiConfig) return;
77
+ const normalized = { ...aiConfig };
78
+ if (asNonEmptyString(normalized.apiKey)) return stripAgentAISecretFields(normalized);
79
+ const secretName = asNonEmptyString(normalized.apiKeySecretName) ?? getDefaultSecretName(normalized);
80
+ if (!secretName || !db) return stripAgentAISecretFields(normalized);
81
+ const tenantId = asNonEmptyString(input.tenantId) ?? asNonEmptyString(getCurrentTenant()?.tenantId);
82
+ if (!tenantId) return stripAgentAISecretFields(normalized);
83
+ const tenantIds = await getTenantSearchOrder(db, tenantId, normalizeSecretFallback(normalized.apiKeySecretFallback));
84
+ const apiKey = await resolveSecretValue(await getSecretService(db), tenantIds, secretName);
85
+ if (!apiKey) return stripAgentAISecretFields(normalized);
86
+ return {
87
+ ...stripAgentAISecretFields(normalized),
88
+ apiKey
89
+ };
127
90
  }
91
+ //#endregion
92
+ //#region src/interests.ts
128
93
  function mergeFilters(globalFilter, objectFilter) {
129
- if (!globalFilter && !objectFilter) return {};
130
- if (!globalFilter) return { ...objectFilter };
131
- if (!objectFilter) return { ...globalFilter };
132
- return { ...globalFilter, ...objectFilter };
94
+ if (!globalFilter && !objectFilter) return {};
95
+ if (!globalFilter) return { ...objectFilter };
96
+ if (!objectFilter) return { ...globalFilter };
97
+ return {
98
+ ...globalFilter,
99
+ ...objectFilter
100
+ };
133
101
  }
134
102
  function normalizeSort(sort) {
135
- if (!sort) return [];
136
- return Array.isArray(sort) ? sort : [sort];
103
+ if (!sort) return [];
104
+ return Array.isArray(sort) ? sort : [sort];
137
105
  }
106
+ //#endregion
107
+ //#region src/agent.ts
138
108
  var __defProp$2 = Object.defineProperty;
139
109
  var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
140
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
110
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
111
+ enumerable: true,
112
+ configurable: true,
113
+ writable: true,
114
+ value
115
+ }) : obj[key] = value;
141
116
  var __decorateClass$2 = (decorators, target, key, kind) => {
142
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
143
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
144
- if (decorator = decorators[i])
145
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
146
- if (kind && result) __defProp$2(target, key, result);
147
- return result;
117
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
118
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
119
+ if (kind && result) __defProp$2(target, key, result);
120
+ return result;
148
121
  };
149
122
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
150
- let Agent = class extends SmrtObject {
151
- tenantId = null;
152
- /**
153
- * Current agent status
154
- */
155
- status = "idle";
156
- /**
157
- * Structured logger instance
158
- * Created with agent's class name as context
159
- */
160
- logger;
161
- /**
162
- * Signal handlers for graceful shutdown
163
- */
164
- signalHandlers = /* @__PURE__ */ new Map();
165
- /**
166
- * Cached DispatchBus instance for inter-agent communication
167
- */
168
- _dispatch = null;
169
- /**
170
- * Creates a new Agent instance
171
- *
172
- * @param options - Configuration options including identifiers and metadata
173
- */
174
- constructor(options = {}) {
175
- super(options);
176
- this.logger = createLogger(options.silent ? false : { level: "info" });
177
- }
178
- /**
179
- * Interest configuration for this agent
180
- * Lazily accessed from options on first interesting() call
181
- */
182
- get interests() {
183
- return this.options.interests;
184
- }
185
- /**
186
- * Canonical agent type for persistence and dispatch routing.
187
- */
188
- getAgentTypeName() {
189
- const metaType = this._meta_type;
190
- if (typeof metaType === "string" && metaType.length > 0) {
191
- return getAgentTypeName(metaType);
192
- }
193
- return getAgentTypeName(this.constructor.name);
194
- }
195
- /**
196
- * Human-readable class name for logs and UI.
197
- */
198
- getAgentClassName() {
199
- return getAgentClassName(this.getAgentTypeName());
200
- }
201
- /**
202
- * Get UI slot definitions for this agent instance
203
- *
204
- * Returns the static uiSlots defined on the agent's class.
205
- * Used by host applications to discover available admin panels.
206
- *
207
- * @example
208
- * ```typescript
209
- * const slots = agent.getUISlots();
210
- * for (const [slotId, slot] of Object.entries(slots)) {
211
- * console.log(`${slot.label}: ${slot.description}`);
212
- * }
213
- * ```
214
- */
215
- getUISlots() {
216
- return this.constructor.uiSlots;
217
- }
218
- // ============================================================================
219
- // Configuration Management
220
- // ============================================================================
221
- /**
222
- * Load all database-persisted configs for this agent
223
- *
224
- * Returns a Map of slotId → configData for all saved configurations.
225
- * Use getMergedConfig() to get file + db merged config for a slot.
226
- *
227
- * @returns Map of slotId to config data
228
- *
229
- * @example
230
- * ```typescript
231
- * const configs = await agent.loadConfigs();
232
- * const sources = configs.get('sources');
233
- * ```
234
- */
235
- async loadConfigs() {
236
- if (!this.id) {
237
- throw new Error("Agent must be saved before loading configs");
238
- }
239
- return AgentConfig.forAgent(this.id, this.options);
240
- }
241
- /**
242
- * Save config for a specific UI slot to the database
243
- *
244
- * Persists configuration data that can be modified by admin panels.
245
- * Use this when the user saves changes in an admin UI.
246
- *
247
- * @param slotId - The UI slot ID (e.g., 'sources', 'settings')
248
- * @param data - Configuration data to save
249
- *
250
- * @example
251
- * ```typescript
252
- * await agent.saveSlotConfig('sources', {
253
- * scrapers: ['civicweb', 'govstack'],
254
- * refreshInterval: 3600
255
- * });
256
- * ```
257
- */
258
- async saveSlotConfig(slotId, data) {
259
- if (!this.id) {
260
- throw new Error("Agent must be saved before saving slot config");
261
- }
262
- await AgentConfig.saveSlot(
263
- {
264
- agentId: this.id,
265
- agentClass: this.getAgentTypeName(),
266
- slotId,
267
- configData: data
268
- },
269
- this.options
270
- );
271
- }
272
- /**
273
- * Get merged config for a slot (file-based + database)
274
- *
275
- * Priority order (highest to lowest):
276
- * 1. Database-persisted config (from saveSlotConfig)
277
- * 2. File-based config (from getModuleConfig)
278
- * 3. Agent class defaults
279
- *
280
- * @param slotId - The UI slot ID
281
- * @returns Merged configuration object
282
- *
283
- * @example
284
- * ```typescript
285
- * const sourcesConfig = await agent.getMergedConfig('sources');
286
- * // Returns file config merged with any db overrides
287
- * ```
288
- */
289
- async getMergedConfig(slotId) {
290
- const fileConfig = this.config?.[slotId] ?? {};
291
- if (!this.id) {
292
- return fileConfig;
293
- }
294
- const dbConfig = await AgentConfig.forSlot(this.id, slotId, this.options);
295
- return { ...fileConfig, ...dbConfig ?? {} };
296
- }
297
- /**
298
- * Export all config for this agent (for static site generation)
299
- *
300
- * Merges file-based and database configs, then optionally sanitizes
301
- * to remove secrets. Use this before building a static site.
302
- *
303
- * @param options - Export options
304
- * @param options.includeSecrets - If true, includes API keys and secrets (default: false)
305
- * @returns Merged configuration object
306
- *
307
- * @example
308
- * ```typescript
309
- * // Export for static build (secrets filtered)
310
- * const config = await agent.exportConfig();
311
- *
312
- * // Export with secrets (for secure environments)
313
- * const fullConfig = await agent.exportConfig({ includeSecrets: true });
314
- * ```
315
- */
316
- async exportConfig(options) {
317
- const dbConfigs = await this.loadConfigs();
318
- const fileConfig = this.config ?? {};
319
- const merged = { ...fileConfig };
320
- for (const [slotId, data] of dbConfigs) {
321
- merged[slotId] = {
322
- ...merged[slotId],
323
- ...data
324
- };
325
- }
326
- if (!options?.includeSecrets) {
327
- return sanitizeConfig(merged);
328
- }
329
- return merged;
330
- }
331
- /**
332
- * Get the DispatchBus for inter-agent communication
333
- *
334
- * Creates a DispatchBus lazily on first access. Requires database configuration.
335
- *
336
- * @example
337
- * ```typescript
338
- * // Emit a dispatch to other agents
339
- * await this.dispatch.emit('campaign.completed', {
340
- * campaignId: '123',
341
- * revenue: 5000
342
- * }, { source: this.constructor.name });
343
- *
344
- * // Subscribe to dispatches
345
- * await this.dispatch.subscribe({
346
- * signalType: 'campaign.*',
347
- * subscriber: this.constructor.name
348
- * });
349
- * ```
350
- *
351
- * @throws Error if database is not configured
352
- */
353
- async getDispatch() {
354
- if (!this._dispatch) {
355
- if (!this._db) {
356
- throw new Error(
357
- `Agent ${this.constructor.name} requires database configuration for dispatch. Ensure the agent is initialized with a db option.`
358
- );
359
- }
360
- this._dispatch = await createDispatchBus({
361
- db: this._db
362
- });
363
- }
364
- return this._dispatch;
365
- }
366
- /**
367
- * Handle incoming dispatches
368
- *
369
- * Override this method to process dispatches targeted at this agent.
370
- * Called when process() is invoked for this agent's subscriber name.
371
- *
372
- * @param payload - Dispatch payload data
373
- * @param metadata - Dispatch metadata including type, source, and timing
374
- *
375
- * @example
376
- * ```typescript
377
- * async handleDispatch(payload: unknown, metadata: DispatchMetadata): Promise<void> {
378
- * if (metadata.type === 'campaign.completed') {
379
- * const data = payload as { campaignId: string; revenue: number };
380
- * await this.recordRevenue(data.campaignId, data.revenue);
381
- * }
382
- * }
383
- * ```
384
- */
385
- async handleDispatch(_payload, _metadata) {
386
- }
387
- /**
388
- * Process pending dispatches for this agent
389
- *
390
- * Finds and processes all pending dispatches that match this agent's subscriptions.
391
- * Uses handleDispatch() to process each dispatch.
392
- *
393
- * @returns Number of dispatches processed
394
- *
395
- * @example
396
- * ```typescript
397
- * // In your run() method
398
- * const processed = await this.processDispatches();
399
- * this.logger.info(`Processed ${processed} dispatches`);
400
- * ```
401
- */
402
- async processDispatches() {
403
- const dispatch = await this.getDispatch();
404
- return dispatch.process(
405
- this.getAgentTypeName(),
406
- this.handleDispatch.bind(this)
407
- );
408
- }
409
- /**
410
- * Initialize the agent
411
- * Sets status to 'initializing' and sets up signal handlers
412
- *
413
- * Override to perform setup after construction, but always call super.initialize()
414
- *
415
- * @example
416
- * ```typescript
417
- * async initialize(): Promise<void> {
418
- * await super.initialize();
419
- * // Custom initialization logic
420
- * }
421
- * ```
422
- */
423
- async initialize() {
424
- await super.initialize();
425
- this.status = "initializing";
426
- this.logger.info("Agent initializing");
427
- const fileAiConfig = typeof this.config === "object" && this.config !== null && "ai" in this.config && typeof this.config.ai === "object" && this.config.ai !== null ? this.config.ai : void 0;
428
- const configuredAi = this.options.ai ?? fileAiConfig;
429
- if (configuredAi && this._db) {
430
- const resolvedAi = await resolveAgentAIOptions({
431
- aiConfig: configuredAi,
432
- db: this._db,
433
- tenantId: getCurrentTenant()?.tenantId || (typeof this.tenantId === "string" ? this.tenantId : void 0)
434
- });
435
- if (resolvedAi) {
436
- this.options.ai = resolvedAi;
437
- }
438
- }
439
- if (this.options.manageProcessSignals) {
440
- this.setupSignalHandlers();
441
- }
442
- if (this._db) {
443
- const dispatch = await this.getDispatch();
444
- await this.migrateLegacyDispatchSubscriptions(dispatch);
445
- const subs = this.constructor.signalSubscriptions;
446
- if (subs.length > 0) {
447
- const subscriber = this.getAgentTypeName();
448
- const existing = await dispatch.listSubscriptions(subscriber);
449
- const existingTypes = new Set(existing.map((s) => s.signalType));
450
- for (const signalType of subs) {
451
- if (!existingTypes.has(signalType)) {
452
- await dispatch.subscribe({
453
- signalType,
454
- subscriber
455
- });
456
- }
457
- }
458
- }
459
- }
460
- return this;
461
- }
462
- /**
463
- * Set up signal handlers for graceful shutdown
464
- * Handles SIGTERM and SIGINT for single-agent processes that explicitly opt in.
465
- */
466
- setupSignalHandlers() {
467
- const signals = ["SIGTERM", "SIGINT"];
468
- for (const signal of signals) {
469
- const handler = () => {
470
- this.logger.info(`Received ${signal}, shutting down gracefully`);
471
- this.shutdown().then(() => {
472
- process.exit(0);
473
- }).catch((error) => {
474
- this.logger.error("Error during shutdown", { error });
475
- process.exit(1);
476
- });
477
- };
478
- this.signalHandlers.set(signal, handler);
479
- process.on(signal, handler);
480
- }
481
- }
482
- /**
483
- * Migrate legacy simple-name dispatch subscribers to the canonical agent type.
484
- *
485
- * Older releases used `this.constructor.name` directly for subscriber IDs.
486
- * That collides across packages and leaves fan-out dispatches targeted at the
487
- * wrong subscriber once qualified names are available.
488
- */
489
- async migrateLegacyDispatchSubscriptions(dispatch) {
490
- if (!this._db) {
491
- return;
492
- }
493
- const legacySubscriber = this.constructor.name;
494
- const canonicalSubscriber = this.getAgentTypeName();
495
- if (legacySubscriber === canonicalSubscriber) {
496
- return;
497
- }
498
- const legacySubscriptions = await dispatch.listSubscriptions(legacySubscriber);
499
- if (legacySubscriptions.length === 0) {
500
- return;
501
- }
502
- const currentSubscriptions = await dispatch.listSubscriptions(canonicalSubscriber);
503
- const currentSignalTypes = new Set(
504
- currentSubscriptions.map((sub) => sub.signalType)
505
- );
506
- for (const subscription of legacySubscriptions) {
507
- if (!currentSignalTypes.has(subscription.signalType)) {
508
- await dispatch.subscribe({
509
- signalType: subscription.signalType,
510
- subscriber: canonicalSubscriber,
511
- handler: subscription.handler,
512
- delivery: subscription.delivery,
513
- enabled: subscription.enabled
514
- });
515
- }
516
- await dispatch.unsubscribe(subscription.signalType, legacySubscriber);
517
- }
518
- const [tenantClause, tenantParams] = buildDispatchTenantUpdatePredicate(
519
- resolveDispatchTenantScope()
520
- );
521
- await this._db.query(
522
- `UPDATE _smrt_dispatch
123
+ var Agent = class extends SmrtObject {
124
+ tenantId = null;
125
+ /**
126
+ * Current agent status
127
+ */
128
+ status = "idle";
129
+ /**
130
+ * Structured logger instance
131
+ * Created with agent's class name as context
132
+ */
133
+ logger;
134
+ /**
135
+ * Signal handlers for graceful shutdown
136
+ */
137
+ signalHandlers = /* @__PURE__ */ new Map();
138
+ /**
139
+ * Cached DispatchBus instance for inter-agent communication
140
+ */
141
+ _dispatch = null;
142
+ /**
143
+ * Creates a new Agent instance
144
+ *
145
+ * @param options - Configuration options including identifiers and metadata
146
+ */
147
+ constructor(options = {}) {
148
+ super(options);
149
+ this.logger = createLogger(options.silent ? false : { level: "info" });
150
+ }
151
+ /**
152
+ * Interest configuration for this agent
153
+ * Lazily accessed from options on first interesting() call
154
+ */
155
+ get interests() {
156
+ return this.options.interests;
157
+ }
158
+ /**
159
+ * Canonical agent type for persistence and dispatch routing.
160
+ */
161
+ getAgentTypeName() {
162
+ const metaType = this._meta_type;
163
+ if (typeof metaType === "string" && metaType.length > 0) return getAgentTypeName(metaType);
164
+ return getAgentTypeName(this.constructor.name);
165
+ }
166
+ /**
167
+ * Human-readable class name for logs and UI.
168
+ */
169
+ getAgentClassName() {
170
+ return getAgentClassName(this.getAgentTypeName());
171
+ }
172
+ /**
173
+ * Get UI slot definitions for this agent instance
174
+ *
175
+ * Returns the static uiSlots defined on the agent's class.
176
+ * Used by host applications to discover available admin panels.
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const slots = agent.getUISlots();
181
+ * for (const [slotId, slot] of Object.entries(slots)) {
182
+ * console.log(`${slot.label}: ${slot.description}`);
183
+ * }
184
+ * ```
185
+ */
186
+ getUISlots() {
187
+ return this.constructor.uiSlots;
188
+ }
189
+ /**
190
+ * Load all database-persisted configs for this agent
191
+ *
192
+ * Returns a Map of slotId → configData for all saved configurations.
193
+ * Use getMergedConfig() to get file + db merged config for a slot.
194
+ *
195
+ * @returns Map of slotId to config data
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const configs = await agent.loadConfigs();
200
+ * const sources = configs.get('sources');
201
+ * ```
202
+ */
203
+ async loadConfigs() {
204
+ if (!this.id) throw new Error("Agent must be saved before loading configs");
205
+ return AgentConfig.forAgent(this.id, this.options);
206
+ }
207
+ /**
208
+ * Save config for a specific UI slot to the database
209
+ *
210
+ * Persists configuration data that can be modified by admin panels.
211
+ * Use this when the user saves changes in an admin UI.
212
+ *
213
+ * @param slotId - The UI slot ID (e.g., 'sources', 'settings')
214
+ * @param data - Configuration data to save
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * await agent.saveSlotConfig('sources', {
219
+ * scrapers: ['civicweb', 'govstack'],
220
+ * refreshInterval: 3600
221
+ * });
222
+ * ```
223
+ */
224
+ async saveSlotConfig(slotId, data) {
225
+ if (!this.id) throw new Error("Agent must be saved before saving slot config");
226
+ await AgentConfig.saveSlot({
227
+ agentId: this.id,
228
+ agentClass: this.getAgentTypeName(),
229
+ slotId,
230
+ configData: data
231
+ }, this.options);
232
+ }
233
+ /**
234
+ * Get merged config for a slot (file-based + database)
235
+ *
236
+ * Priority order (highest to lowest):
237
+ * 1. Database-persisted config (from saveSlotConfig)
238
+ * 2. File-based config (from getModuleConfig)
239
+ * 3. Agent class defaults
240
+ *
241
+ * @param slotId - The UI slot ID
242
+ * @returns Merged configuration object
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * const sourcesConfig = await agent.getMergedConfig('sources');
247
+ * // Returns file config merged with any db overrides
248
+ * ```
249
+ */
250
+ async getMergedConfig(slotId) {
251
+ const fileConfig = this.config?.[slotId] ?? {};
252
+ if (!this.id) return fileConfig;
253
+ const dbConfig = await AgentConfig.forSlot(this.id, slotId, this.options);
254
+ return {
255
+ ...fileConfig,
256
+ ...dbConfig ?? {}
257
+ };
258
+ }
259
+ /**
260
+ * Export all config for this agent (for static site generation)
261
+ *
262
+ * Merges file-based and database configs, then optionally sanitizes
263
+ * to remove secrets. Use this before building a static site.
264
+ *
265
+ * @param options - Export options
266
+ * @param options.includeSecrets - If true, includes API keys and secrets (default: false)
267
+ * @returns Merged configuration object
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // Export for static build (secrets filtered)
272
+ * const config = await agent.exportConfig();
273
+ *
274
+ * // Export with secrets (for secure environments)
275
+ * const fullConfig = await agent.exportConfig({ includeSecrets: true });
276
+ * ```
277
+ */
278
+ async exportConfig(options) {
279
+ const dbConfigs = await this.loadConfigs();
280
+ const merged = { ...this.config ?? {} };
281
+ for (const [slotId, data] of dbConfigs) merged[slotId] = {
282
+ ...merged[slotId],
283
+ ...data
284
+ };
285
+ if (!options?.includeSecrets) return sanitizeConfig(merged);
286
+ return merged;
287
+ }
288
+ /**
289
+ * Get the DispatchBus for inter-agent communication
290
+ *
291
+ * Creates a DispatchBus lazily on first access. Requires database configuration.
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * // Emit a dispatch to other agents
296
+ * await this.dispatch.emit('campaign.completed', {
297
+ * campaignId: '123',
298
+ * revenue: 5000
299
+ * }, { source: this.constructor.name });
300
+ *
301
+ * // Subscribe to dispatches
302
+ * await this.dispatch.subscribe({
303
+ * signalType: 'campaign.*',
304
+ * subscriber: this.constructor.name
305
+ * });
306
+ * ```
307
+ *
308
+ * @throws Error if database is not configured
309
+ */
310
+ async getDispatch() {
311
+ if (!this._dispatch) {
312
+ if (!this._db) throw new Error(`Agent ${this.constructor.name} requires database configuration for dispatch. Ensure the agent is initialized with a db option.`);
313
+ this._dispatch = await createDispatchBus({ db: this._db });
314
+ }
315
+ return this._dispatch;
316
+ }
317
+ /**
318
+ * Handle incoming dispatches
319
+ *
320
+ * Override this method to process dispatches targeted at this agent.
321
+ * Called when process() is invoked for this agent's subscriber name.
322
+ *
323
+ * @param payload - Dispatch payload data
324
+ * @param metadata - Dispatch metadata including type, source, and timing
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * async handleDispatch(payload: unknown, metadata: DispatchMetadata): Promise<void> {
329
+ * if (metadata.type === 'campaign.completed') {
330
+ * const data = payload as { campaignId: string; revenue: number };
331
+ * await this.recordRevenue(data.campaignId, data.revenue);
332
+ * }
333
+ * }
334
+ * ```
335
+ */
336
+ async handleDispatch(_payload, _metadata) {}
337
+ /**
338
+ * Process pending dispatches for this agent
339
+ *
340
+ * Finds and processes all pending dispatches that match this agent's subscriptions.
341
+ * Uses handleDispatch() to process each dispatch.
342
+ *
343
+ * @returns Number of dispatches processed
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * // In your run() method
348
+ * const processed = await this.processDispatches();
349
+ * this.logger.info(`Processed ${processed} dispatches`);
350
+ * ```
351
+ */
352
+ async processDispatches() {
353
+ return (await this.getDispatch()).process(this.getAgentTypeName(), this.handleDispatch.bind(this));
354
+ }
355
+ /**
356
+ * Initialize the agent
357
+ * Sets status to 'initializing' and sets up signal handlers
358
+ *
359
+ * Override to perform setup after construction, but always call super.initialize()
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * async initialize(): Promise<void> {
364
+ * await super.initialize();
365
+ * // Custom initialization logic
366
+ * }
367
+ * ```
368
+ */
369
+ async initialize() {
370
+ await super.initialize();
371
+ this.status = "initializing";
372
+ this.logger.info("Agent initializing");
373
+ const fileAiConfig = typeof this.config === "object" && this.config !== null && "ai" in this.config && typeof this.config.ai === "object" && this.config.ai !== null ? this.config.ai : void 0;
374
+ const configuredAi = this.options.ai ?? fileAiConfig;
375
+ if (configuredAi && this._db) {
376
+ const resolvedAi = await resolveAgentAIOptions({
377
+ aiConfig: configuredAi,
378
+ db: this._db,
379
+ tenantId: getCurrentTenant()?.tenantId || (typeof this.tenantId === "string" ? this.tenantId : void 0)
380
+ });
381
+ if (resolvedAi) this.options.ai = resolvedAi;
382
+ }
383
+ if (this.options.manageProcessSignals) this.setupSignalHandlers();
384
+ if (this._db) {
385
+ const dispatch = await this.getDispatch();
386
+ await this.migrateLegacyDispatchSubscriptions(dispatch);
387
+ const subs = this.constructor.signalSubscriptions;
388
+ if (subs.length > 0) {
389
+ const subscriber = this.getAgentTypeName();
390
+ const existing = await dispatch.listSubscriptions(subscriber);
391
+ const existingTypes = new Set(existing.map((s) => s.signalType));
392
+ for (const signalType of subs) if (!existingTypes.has(signalType)) await dispatch.subscribe({
393
+ signalType,
394
+ subscriber
395
+ });
396
+ }
397
+ }
398
+ return this;
399
+ }
400
+ /**
401
+ * Set up signal handlers for graceful shutdown
402
+ * Handles SIGTERM and SIGINT for single-agent processes that explicitly opt in.
403
+ */
404
+ setupSignalHandlers() {
405
+ for (const signal of ["SIGTERM", "SIGINT"]) {
406
+ const handler = () => {
407
+ this.logger.info(`Received ${signal}, shutting down gracefully`);
408
+ this.shutdown().then(() => {
409
+ process.exit(0);
410
+ }).catch((error) => {
411
+ this.logger.error("Error during shutdown", { error });
412
+ process.exit(1);
413
+ });
414
+ };
415
+ this.signalHandlers.set(signal, handler);
416
+ process.on(signal, handler);
417
+ }
418
+ }
419
+ /**
420
+ * Migrate legacy simple-name dispatch subscribers to the canonical agent type.
421
+ *
422
+ * Older releases used `this.constructor.name` directly for subscriber IDs.
423
+ * That collides across packages and leaves fan-out dispatches targeted at the
424
+ * wrong subscriber once qualified names are available.
425
+ */
426
+ async migrateLegacyDispatchSubscriptions(dispatch) {
427
+ if (!this._db) return;
428
+ const legacySubscriber = this.constructor.name;
429
+ const canonicalSubscriber = this.getAgentTypeName();
430
+ if (legacySubscriber === canonicalSubscriber) return;
431
+ const legacySubscriptions = await dispatch.listSubscriptions(legacySubscriber);
432
+ if (legacySubscriptions.length === 0) return;
433
+ const currentSubscriptions = await dispatch.listSubscriptions(canonicalSubscriber);
434
+ const currentSignalTypes = new Set(currentSubscriptions.map((sub) => sub.signalType));
435
+ for (const subscription of legacySubscriptions) {
436
+ if (!currentSignalTypes.has(subscription.signalType)) await dispatch.subscribe({
437
+ signalType: subscription.signalType,
438
+ subscriber: canonicalSubscriber,
439
+ handler: subscription.handler,
440
+ delivery: subscription.delivery,
441
+ enabled: subscription.enabled
442
+ });
443
+ await dispatch.unsubscribe(subscription.signalType, legacySubscriber);
444
+ }
445
+ const [tenantClause, tenantParams] = buildDispatchTenantUpdatePredicate(resolveDispatchTenantScope());
446
+ await this._db.query(`UPDATE _smrt_dispatch
523
447
  SET target_subscriber = CASE
524
448
  WHEN target_subscriber = ? THEN ?
525
449
  ELSE target_subscriber
@@ -528,908 +452,842 @@ let Agent = class extends SmrtObject {
528
452
  WHEN processed_by = ? THEN ?
529
453
  ELSE processed_by
530
454
  END
531
- WHERE (target_subscriber = ? OR processed_by = ?)${tenantClause}`,
532
- legacySubscriber,
533
- canonicalSubscriber,
534
- legacySubscriber,
535
- canonicalSubscriber,
536
- legacySubscriber,
537
- legacySubscriber,
538
- ...tenantParams
539
- );
540
- }
541
- /**
542
- * Clean up signal handlers
543
- */
544
- cleanupSignalHandlers() {
545
- for (const [signal, handler] of this.signalHandlers.entries()) {
546
- process.removeListener(signal, handler);
547
- }
548
- this.signalHandlers.clear();
549
- }
550
- /**
551
- * Validate configuration and dependencies
552
- * Override to check agent-specific requirements
553
- *
554
- * @throws Error if validation fails
555
- *
556
- * @example
557
- * ```typescript
558
- * async validate(): Promise<void> {
559
- * if (!this.config.apiKey) {
560
- * throw new Error('API key is required');
561
- * }
562
- * }
563
- * ```
564
- */
565
- async validate() {
566
- this.logger.info("Validating agent configuration");
567
- }
568
- /**
569
- * Cleanup and shutdown
570
- * Override to perform graceful shutdown
571
- *
572
- * Always call super.shutdown() to clean up signal handlers
573
- *
574
- * @example
575
- * ```typescript
576
- * async shutdown(): Promise<void> {
577
- * this.logger.info('Cleaning up resources');
578
- * await this.cleanup();
579
- * await super.shutdown();
580
- * }
581
- * ```
582
- */
583
- async shutdown() {
584
- this.status = "shutdown";
585
- this.logger.info("Agent shutting down");
586
- this.cleanupSignalHandlers();
587
- }
588
- /**
589
- * Execute agent with lifecycle management
590
- *
591
- * Runs the full lifecycle:
592
- * 1. initialize() — seeds signal subscriptions if declared
593
- * 2. validate()
594
- * 3. processDispatches() — auto-processes pending dispatches if subscriptions exist
595
- * 4. run()
596
- *
597
- * Note: handleDispatch() callbacks may fire before run() is entered.
598
- *
599
- * On error:
600
- * 1. Sets status to 'error'
601
- * 2. Logs error
602
- * 3. Re-throws error
603
- *
604
- * @example
605
- * ```typescript
606
- * const agent = new MyAgent({ name: 'my-agent' });
607
- *
608
- * try {
609
- * await agent.execute();
610
- * console.log('Agent completed successfully');
611
- * } catch (error) {
612
- * console.error('Agent failed:', error);
613
- * }
614
- * ```
615
- */
616
- async execute() {
617
- try {
618
- await this.initialize();
619
- await this.validate();
620
- this.status = "running";
621
- if (this._db) {
622
- const dispatch = await this.getDispatch();
623
- const subs = await dispatch.listSubscriptions(this.getAgentTypeName());
624
- if (subs.length > 0) {
625
- const count = await this.processDispatches();
626
- if (count > 0) {
627
- this.logger.info(`Processed ${count} pending dispatches`);
628
- }
629
- }
630
- }
631
- await this.run();
632
- this.status = "idle";
633
- this.logger.info("Agent execution completed");
634
- } catch (error) {
635
- this.status = "error";
636
- this.logger.error("Agent execution failed", { error });
637
- throw error;
638
- }
639
- }
640
- /**
641
- * Query objects this agent is interested in
642
- *
643
- * Returns items from all configured object types, filtered and sorted
644
- * according to interest configuration. If handlers are defined on filters,
645
- * they are called for each matched item and the result is included.
646
- *
647
- * @returns Array of { type, data, name?, handled? } results
648
- * @throws Error if no interests are configured
649
- *
650
- * @example
651
- * ```typescript
652
- * const items = await this.interesting();
653
- * for (const { type, data, name, handled } of items) {
654
- * console.log(`Processing ${type} from "${name}": action=${handled?.action}`);
655
- * }
656
- * ```
657
- */
658
- async interesting() {
659
- if (!this.interests) {
660
- throw new Error(
661
- `Agent ${this.constructor.name} has no interests configured. Set interests in constructor options to use interesting().`
662
- );
663
- }
664
- if (!this.interests.objects || Object.keys(this.interests.objects).length === 0) {
665
- this.logger.warn("Agent has empty interests.objects configuration");
666
- return [];
667
- }
668
- const results = [];
669
- for (const [className, config] of Object.entries(this.interests.objects)) {
670
- try {
671
- const items = await this.queryInterestingObjects(className, config);
672
- results.push(...items);
673
- } catch (error) {
674
- this.logger.warn(`Failed to query ${className} for interests`, {
675
- error
676
- });
677
- }
678
- }
679
- if (this.interests.qualify) {
680
- const allItems = results.map((r) => r.data);
681
- const qualified = await this.interests.qualify(allItems);
682
- const qualifiedSet = new Set(qualified);
683
- const filteredResults = results.filter((r) => qualifiedSet.has(r.data));
684
- if (this.interests.sort) {
685
- return this.sortResults(filteredResults, this.interests.sort);
686
- }
687
- return filteredResults;
688
- }
689
- if (this.interests.sort) {
690
- return this.sortResults(results, this.interests.sort);
691
- }
692
- return results;
693
- }
694
- /**
695
- * Query a single object type based on interest config
696
- *
697
- * Supports both single filter and array of filters.
698
- * Each filter can use standard SDK filters OR custom query function.
699
- * Returns InterestResult[] with handler results included.
700
- */
701
- async queryInterestingObjects(className, config) {
702
- if (!ObjectRegistry.hasClass(className)) {
703
- this.logger.warn(
704
- `Object type "${className}" not found in ObjectRegistry. Skipping in interests query.`
705
- );
706
- return [];
707
- }
708
- const collection = await ObjectRegistry.getCollection(
709
- className,
710
- this.options
711
- );
712
- const filters = this.normalizeInterestConfig(config);
713
- const allResults = [];
714
- for (const filter of filters) {
715
- const items = await this.queryInterestFilter(
716
- className,
717
- filter,
718
- collection
719
- );
720
- for (const item of items) {
721
- const result = {
722
- type: className,
723
- data: item,
724
- name: filter.name
725
- };
726
- if (filter.handler) {
727
- result.handled = await filter.handler(item, this);
728
- }
729
- allResults.push(result);
730
- }
731
- }
732
- return allResults;
733
- }
734
- /**
735
- * Normalize ObjectInterestConfig to array format
736
- */
737
- normalizeInterestConfig(config) {
738
- return Array.isArray(config) ? config : [config];
739
- }
740
- /**
741
- * Query a single interest filter
742
- *
743
- * Uses collection.query() for custom query functions,
744
- * or collection.list() for standard SDK filters.
745
- */
746
- async queryInterestFilter(_className, filter, collection) {
747
- if (filter.query) {
748
- let [whereClause, params] = filter.query(collection.tableName);
749
- await ObjectRegistry.ensureManifestLoaded(_className);
750
- let currentClass = ObjectRegistry.getClass(_className);
751
- while (currentClass?.extends) {
752
- const parentName = currentClass.extends;
753
- if (parentName === "SmrtObject" || parentName === "SmrtClass" || parentName === "SmrtCollection") {
754
- break;
755
- }
756
- try {
757
- await ObjectRegistry.ensureManifestLoaded(parentName);
758
- } catch {
759
- }
760
- currentClass = ObjectRegistry.getClass(parentName);
761
- }
762
- ObjectRegistry.invalidateInheritanceCache(_className);
763
- const tableStrategy = ObjectRegistry.getTableStrategy(_className);
764
- if (tableStrategy === "sti") {
765
- const stiBase = ObjectRegistry.getSTIBase(_className);
766
- const classInfo = ObjectRegistry.getClass(_className);
767
- const qualifiedClassName = classInfo?.qualifiedName ?? classInfo?.name ?? _className;
768
- if (stiBase && stiBase !== qualifiedClassName && stiBase !== _className) {
769
- const metaTypeValue = classInfo?.qualifiedName || _className;
770
- whereClause = `_meta_type = ? AND (${whereClause})`;
771
- params = [metaTypeValue, ...params];
772
- }
773
- }
774
- let sql = `SELECT * FROM ${collection.tableName} WHERE ${whereClause}`;
775
- if (filter.sort) {
776
- const sorts = Array.isArray(filter.sort) ? filter.sort : [filter.sort];
777
- const orderBy = sorts.map((item) => {
778
- const [field2, direction = "ASC"] = item.trim().split(/\s+/);
779
- if (!/^[a-zA-Z0-9_]+$/.test(field2)) {
780
- throw new Error(`Invalid field name for ordering: ${field2}`);
781
- }
782
- const normalizedDirection = direction.toUpperCase();
783
- if (normalizedDirection !== "ASC" && normalizedDirection !== "DESC") {
784
- throw new Error(
785
- `Invalid sort direction: ${direction}. Must be ASC or DESC.`
786
- );
787
- }
788
- return `${field2} ${normalizedDirection}`;
789
- }).join(", ");
790
- sql += ` ORDER BY ${orderBy}`;
791
- }
792
- if (filter.limit) {
793
- sql += ` LIMIT ?`;
794
- params.push(filter.limit);
795
- }
796
- let items2 = await collection.query(sql, params);
797
- if (filter.qualify) {
798
- items2 = await filter.qualify(items2);
799
- }
800
- return items2;
801
- }
802
- const mergedFilter = mergeFilters(this.interests?.filter, filter.filter);
803
- const queryOptions = {};
804
- if (Object.keys(mergedFilter).length > 0) {
805
- queryOptions.where = mergedFilter;
806
- }
807
- if (filter.sort) {
808
- queryOptions.orderBy = filter.sort;
809
- }
810
- if (filter.limit) {
811
- queryOptions.limit = filter.limit;
812
- }
813
- let items = await collection.list(queryOptions);
814
- if (filter.qualify) {
815
- items = await filter.qualify(items);
816
- }
817
- return items;
818
- }
819
- /**
820
- * Sort results by field(s) across all types
821
- */
822
- sortResults(results, sort) {
823
- const sortFields = normalizeSort(sort);
824
- if (sortFields.length === 0) return results;
825
- return [...results].sort((a, b) => {
826
- for (const sortField of sortFields) {
827
- const [field2, direction = "ASC"] = sortField.trim().split(/\s+/);
828
- const aValue = a.data[field2];
829
- const bValue = b.data[field2];
830
- let comparison = 0;
831
- if (aValue < bValue) comparison = -1;
832
- else if (aValue > bValue) comparison = 1;
833
- if (comparison !== 0) {
834
- return direction.toUpperCase() === "DESC" ? -comparison : comparison;
835
- }
836
- }
837
- return 0;
838
- });
839
- }
455
+ WHERE (target_subscriber = ? OR processed_by = ?)${tenantClause}`, legacySubscriber, canonicalSubscriber, legacySubscriber, canonicalSubscriber, legacySubscriber, legacySubscriber, ...tenantParams);
456
+ }
457
+ /**
458
+ * Clean up signal handlers
459
+ */
460
+ cleanupSignalHandlers() {
461
+ for (const [signal, handler] of this.signalHandlers.entries()) process.removeListener(signal, handler);
462
+ this.signalHandlers.clear();
463
+ }
464
+ /**
465
+ * Validate configuration and dependencies
466
+ * Override to check agent-specific requirements
467
+ *
468
+ * @throws Error if validation fails
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * async validate(): Promise<void> {
473
+ * if (!this.config.apiKey) {
474
+ * throw new Error('API key is required');
475
+ * }
476
+ * }
477
+ * ```
478
+ */
479
+ async validate() {
480
+ this.logger.info("Validating agent configuration");
481
+ }
482
+ /**
483
+ * Cleanup and shutdown
484
+ * Override to perform graceful shutdown
485
+ *
486
+ * Always call super.shutdown() to clean up signal handlers
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * async shutdown(): Promise<void> {
491
+ * this.logger.info('Cleaning up resources');
492
+ * await this.cleanup();
493
+ * await super.shutdown();
494
+ * }
495
+ * ```
496
+ */
497
+ async shutdown() {
498
+ this.status = "shutdown";
499
+ this.logger.info("Agent shutting down");
500
+ this.cleanupSignalHandlers();
501
+ }
502
+ /**
503
+ * Execute agent with lifecycle management
504
+ *
505
+ * Runs the full lifecycle:
506
+ * 1. initialize() — seeds signal subscriptions if declared
507
+ * 2. validate()
508
+ * 3. processDispatches() — auto-processes pending dispatches if subscriptions exist
509
+ * 4. run()
510
+ *
511
+ * Note: handleDispatch() callbacks may fire before run() is entered.
512
+ *
513
+ * On error:
514
+ * 1. Sets status to 'error'
515
+ * 2. Logs error
516
+ * 3. Re-throws error
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * const agent = new MyAgent({ name: 'my-agent' });
521
+ *
522
+ * try {
523
+ * await agent.execute();
524
+ * console.log('Agent completed successfully');
525
+ * } catch (error) {
526
+ * console.error('Agent failed:', error);
527
+ * }
528
+ * ```
529
+ */
530
+ async execute() {
531
+ try {
532
+ await this.initialize();
533
+ await this.validate();
534
+ this.status = "running";
535
+ if (this._db) {
536
+ if ((await (await this.getDispatch()).listSubscriptions(this.getAgentTypeName())).length > 0) {
537
+ const count = await this.processDispatches();
538
+ if (count > 0) this.logger.info(`Processed ${count} pending dispatches`);
539
+ }
540
+ }
541
+ await this.run();
542
+ this.status = "idle";
543
+ this.logger.info("Agent execution completed");
544
+ } catch (error) {
545
+ this.status = "error";
546
+ this.logger.error("Agent execution failed", { error });
547
+ throw error;
548
+ }
549
+ }
550
+ /**
551
+ * Query objects this agent is interested in
552
+ *
553
+ * Returns items from all configured object types, filtered and sorted
554
+ * according to interest configuration. If handlers are defined on filters,
555
+ * they are called for each matched item and the result is included.
556
+ *
557
+ * @returns Array of { type, data, name?, handled? } results
558
+ * @throws Error if no interests are configured
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * const items = await this.interesting();
563
+ * for (const { type, data, name, handled } of items) {
564
+ * console.log(`Processing ${type} from "${name}": action=${handled?.action}`);
565
+ * }
566
+ * ```
567
+ */
568
+ async interesting() {
569
+ if (!this.interests) throw new Error(`Agent ${this.constructor.name} has no interests configured. Set interests in constructor options to use interesting().`);
570
+ if (!this.interests.objects || Object.keys(this.interests.objects).length === 0) {
571
+ this.logger.warn("Agent has empty interests.objects configuration");
572
+ return [];
573
+ }
574
+ const results = [];
575
+ for (const [className, config] of Object.entries(this.interests.objects)) try {
576
+ const items = await this.queryInterestingObjects(className, config);
577
+ results.push(...items);
578
+ } catch (error) {
579
+ this.logger.warn(`Failed to query ${className} for interests`, { error });
580
+ }
581
+ if (this.interests.qualify) {
582
+ const allItems = results.map((r) => r.data);
583
+ const qualified = await this.interests.qualify(allItems);
584
+ const qualifiedSet = new Set(qualified);
585
+ const filteredResults = results.filter((r) => qualifiedSet.has(r.data));
586
+ if (this.interests.sort) return this.sortResults(filteredResults, this.interests.sort);
587
+ return filteredResults;
588
+ }
589
+ if (this.interests.sort) return this.sortResults(results, this.interests.sort);
590
+ return results;
591
+ }
592
+ /**
593
+ * Query a single object type based on interest config
594
+ *
595
+ * Supports both single filter and array of filters.
596
+ * Each filter can use standard SDK filters OR custom query function.
597
+ * Returns InterestResult[] with handler results included.
598
+ */
599
+ async queryInterestingObjects(className, config) {
600
+ if (!ObjectRegistry.hasClass(className)) {
601
+ this.logger.warn(`Object type "${className}" not found in ObjectRegistry. Skipping in interests query.`);
602
+ return [];
603
+ }
604
+ const collection = await ObjectRegistry.getCollection(className, this.options);
605
+ const filters = this.normalizeInterestConfig(config);
606
+ const allResults = [];
607
+ for (const filter of filters) {
608
+ const items = await this.queryInterestFilter(className, filter, collection);
609
+ for (const item of items) {
610
+ const result = {
611
+ type: className,
612
+ data: item,
613
+ name: filter.name
614
+ };
615
+ if (filter.handler) result.handled = await filter.handler(item, this);
616
+ allResults.push(result);
617
+ }
618
+ }
619
+ return allResults;
620
+ }
621
+ /**
622
+ * Normalize ObjectInterestConfig to array format
623
+ */
624
+ normalizeInterestConfig(config) {
625
+ return Array.isArray(config) ? config : [config];
626
+ }
627
+ /**
628
+ * Query a single interest filter
629
+ *
630
+ * Uses collection.query() for custom query functions,
631
+ * or collection.list() for standard SDK filters.
632
+ */
633
+ async queryInterestFilter(_className, filter, collection) {
634
+ if (filter.query) {
635
+ let [whereClause, params] = filter.query(collection.tableName);
636
+ await ObjectRegistry.ensureManifestLoaded(_className);
637
+ let currentClass = ObjectRegistry.getClass(_className);
638
+ while (currentClass?.extends) {
639
+ const parentName = currentClass.extends;
640
+ if (parentName === "SmrtObject" || parentName === "SmrtClass" || parentName === "SmrtCollection") break;
641
+ try {
642
+ await ObjectRegistry.ensureManifestLoaded(parentName);
643
+ } catch {}
644
+ currentClass = ObjectRegistry.getClass(parentName);
645
+ }
646
+ ObjectRegistry.invalidateInheritanceCache(_className);
647
+ if (ObjectRegistry.getTableStrategy(_className) === "sti") {
648
+ const stiBase = ObjectRegistry.getSTIBase(_className);
649
+ const classInfo = ObjectRegistry.getClass(_className);
650
+ const qualifiedClassName = classInfo?.qualifiedName ?? classInfo?.name ?? _className;
651
+ if (stiBase && stiBase !== qualifiedClassName && stiBase !== _className) {
652
+ const metaTypeValue = classInfo?.qualifiedName || _className;
653
+ whereClause = `_meta_type = ? AND (${whereClause})`;
654
+ params = [metaTypeValue, ...params];
655
+ }
656
+ }
657
+ let sql = `SELECT * FROM ${collection.tableName} WHERE ${whereClause}`;
658
+ if (filter.sort) {
659
+ const orderBy = (Array.isArray(filter.sort) ? filter.sort : [filter.sort]).map((item) => {
660
+ const [field, direction = "ASC"] = item.trim().split(/\s+/);
661
+ if (!/^[a-zA-Z0-9_]+$/.test(field)) throw new Error(`Invalid field name for ordering: ${field}`);
662
+ const normalizedDirection = direction.toUpperCase();
663
+ if (normalizedDirection !== "ASC" && normalizedDirection !== "DESC") throw new Error(`Invalid sort direction: ${direction}. Must be ASC or DESC.`);
664
+ return `${field} ${normalizedDirection}`;
665
+ }).join(", ");
666
+ sql += ` ORDER BY ${orderBy}`;
667
+ }
668
+ if (filter.limit) {
669
+ sql += ` LIMIT ?`;
670
+ params.push(filter.limit);
671
+ }
672
+ let items2 = await collection.query(sql, params);
673
+ if (filter.qualify) items2 = await filter.qualify(items2);
674
+ return items2;
675
+ }
676
+ const mergedFilter = mergeFilters(this.interests?.filter, filter.filter);
677
+ const queryOptions = {};
678
+ if (Object.keys(mergedFilter).length > 0) queryOptions.where = mergedFilter;
679
+ if (filter.sort) queryOptions.orderBy = filter.sort;
680
+ if (filter.limit) queryOptions.limit = filter.limit;
681
+ let items = await collection.list(queryOptions);
682
+ if (filter.qualify) items = await filter.qualify(items);
683
+ return items;
684
+ }
685
+ /**
686
+ * Sort results by field(s) across all types
687
+ */
688
+ sortResults(results, sort) {
689
+ const sortFields = normalizeSort(sort);
690
+ if (sortFields.length === 0) return results;
691
+ return [...results].sort((a, b) => {
692
+ for (const sortField of sortFields) {
693
+ const [field, direction = "ASC"] = sortField.trim().split(/\s+/);
694
+ const aValue = a.data[field];
695
+ const bValue = b.data[field];
696
+ let comparison = 0;
697
+ if (aValue < bValue) comparison = -1;
698
+ else if (aValue > bValue) comparison = 1;
699
+ if (comparison !== 0) return direction.toUpperCase() === "DESC" ? -comparison : comparison;
700
+ }
701
+ return 0;
702
+ });
703
+ }
840
704
  };
705
+ /**
706
+ * UI slots this agent supports for admin panels
707
+ *
708
+ * Subclasses override this to declare their admin UI slots.
709
+ * Each slot can be implemented by a Svelte component.
710
+ *
711
+ * @example
712
+ * ```typescript
713
+ * static override uiSlots: AgentUISlots = {
714
+ * sources: {
715
+ * id: 'sources',
716
+ * label: 'News Sources',
717
+ * description: 'Configure scrapers and data sources',
718
+ * icon: 'database',
719
+ * order: 1,
720
+ * },
721
+ * settings: {
722
+ * id: 'settings',
723
+ * label: 'Agent Settings',
724
+ * description: 'Configure agent behavior',
725
+ * icon: 'settings',
726
+ * order: 2,
727
+ * },
728
+ * };
729
+ * ```
730
+ */
841
731
  __publicField(Agent, "uiSlots", {});
732
+ /**
733
+ * Admin routes this agent provides
734
+ *
735
+ * Subclasses override this to declare admin route metadata.
736
+ * The vitePluginAgentRoutes Vite plugin reads these from the manifest
737
+ * and registers them so host applications can discover and render them.
738
+ *
739
+ * @example
740
+ * ```typescript
741
+ * static override adminRoutes: AgentAdminRoute[] = [
742
+ * { path: 'sources', component: 'SourcesPanel', load: 'loadSources' },
743
+ * { path: 'sources/[sourceId]', component: 'SourceDetail', load: 'loadSourceDetail' },
744
+ * ];
745
+ * ```
746
+ */
842
747
  __publicField(Agent, "adminRoutes", []);
748
+ /**
749
+ * Signal types this agent subscribes to by default
750
+ *
751
+ * These are seedable defaults — on `initialize()`, the agent checks the
752
+ * database first and only creates subscriptions that don't already exist.
753
+ * The database is the runtime source of truth, allowing users to customize
754
+ * subscriptions per-tenant via the dashboard without code changes.
755
+ *
756
+ * When declared, `execute()` will automatically call `processDispatches()`
757
+ * before `run()`, so handler agents don't need to manually poll.
758
+ * Override `handleDispatch()` to process incoming dispatches.
759
+ *
760
+ * @example
761
+ * ```typescript
762
+ * @smrt({ agent: { icon: 'mail', tier: 'standard' } })
763
+ * class EmailHandler extends Agent {
764
+ * static override signalSubscriptions = ['email.received', 'email.bounced'];
765
+ *
766
+ * async handleDispatch(payload: unknown, metadata: DispatchMetadata) {
767
+ * // Called automatically during execute() for each pending dispatch
768
+ * }
769
+ *
770
+ * async run() { ... }
771
+ * }
772
+ * ```
773
+ */
843
774
  __publicField(Agent, "signalSubscriptions", []);
775
+ /**
776
+ * Execute-time resolvers for `agent_config` fields that should be computed
777
+ * lazily rather than snapshotted at sync time.
778
+ *
779
+ * Each entry is keyed by the agent_config field it produces. The runtime
780
+ * (see {@link resolveLazyConfig}) calls every resolver and overlays the
781
+ * results on top of the persisted config before constructing the agent.
782
+ * That means env-derived values like asset storage paths, S3 buckets, AI
783
+ * provider keys, or tenant-scoped DB URLs stay live: rotating an env var
784
+ * is reflected on the next scheduled run without rewriting the schedule
785
+ * row.
786
+ *
787
+ * Resolvers may be sync or async. Returning `undefined` or `null` leaves
788
+ * the persisted value in place — both are treated as "no overlay" so the
789
+ * common `() => process.env.X ?? null` pattern is safe and won't clobber
790
+ * a snapshotted value when the env var is unset. Throwing falls back to
791
+ * the persisted value (or to whatever
792
+ * {@link ResolveLazyConfigOptions.onError} dictates).
793
+ *
794
+ * @example
795
+ * ```typescript
796
+ * class Praeco extends Agent {
797
+ * static override configResolvers = {
798
+ * assetStorage: () => resolveSharedAssetStorage(),
799
+ * aiKey: async () => loadAIKeyFromSecretsManager(),
800
+ * };
801
+ * }
802
+ * ```
803
+ */
844
804
  __publicField(Agent, "configResolvers", {});
845
- __decorateClass$2([
846
- tenantId({ nullable: true })
847
- ], Agent.prototype, "tenantId", 2);
848
- Agent = __decorateClass$2([
849
- TenantScoped({ mode: "optional" }),
850
- smrt({
851
- // Abstract class - no direct CLI/API/MCP exposure
852
- // But must be registered for inheritance chain to work (issue #523)
853
- cli: false,
854
- api: false,
855
- mcp: false,
856
- // STI: All agents share 'agents' table for polymorphic queries
857
- tableStrategy: "sti"
858
- })
859
- ], Agent);
805
+ __decorateClass$2([tenantId({ nullable: true })], Agent.prototype, "tenantId", 2);
806
+ Agent = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
807
+ cli: false,
808
+ api: false,
809
+ mcp: false,
810
+ tableStrategy: "sti"
811
+ })], Agent);
860
812
  function buildDispatchTenantUpdatePredicate(scope) {
861
- if (!scope.enforced) {
862
- return ["", []];
863
- }
864
- if (scope.tenantId !== null) {
865
- return [" AND (tenant_id = ? OR tenant_id IS NULL)", [scope.tenantId]];
866
- }
867
- return [" AND tenant_id IS NULL", []];
813
+ if (!scope.enforced) return ["", []];
814
+ if (scope.tenantId !== null) return [" AND (tenant_id = ? OR tenant_id IS NULL)", [scope.tenantId]];
815
+ return [" AND tenant_id IS NULL", []];
868
816
  }
817
+ //#endregion
818
+ //#region src/schedule.ts
869
819
  var __defProp$1 = Object.defineProperty;
870
820
  var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
871
821
  var __decorateClass$1 = (decorators, target, key, kind) => {
872
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
873
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
874
- if (decorator = decorators[i])
875
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
876
- if (kind && result) __defProp$1(target, key, result);
877
- return result;
822
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
823
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
824
+ if (kind && result) __defProp$1(target, key, result);
825
+ return result;
878
826
  };
879
- let AgentSchedule = class extends SmrtObject {
880
- tenantId = null;
881
- agentType = "";
882
- agentId = null;
883
- agentConfig = {};
884
- cron = "";
885
- timezone = "UTC";
886
- enabled = true;
887
- status = "active";
888
- lastRun = null;
889
- nextRun = null;
890
- lastStatus = null;
891
- lastError = null;
892
- runCount = 0;
893
- successCount = 0;
894
- failureCount = 0;
895
- maxConcurrent = 1;
896
- runningCount = 0;
897
- timeout = 36e5;
898
- method = "run";
899
- methodArgs = {};
900
- /**
901
- * Enable the schedule
902
- */
903
- async enable() {
904
- this.enabled = true;
905
- this.status = "active";
906
- this.calculateNextRun();
907
- await this.save();
908
- }
909
- /**
910
- * Disable the schedule
911
- */
912
- async disable() {
913
- this.enabled = false;
914
- this.status = "disabled";
915
- await this.save();
916
- }
917
- /**
918
- * Pause the schedule temporarily
919
- */
920
- async pause() {
921
- this.status = "paused";
922
- await this.save();
923
- }
924
- /**
925
- * Resume a paused schedule
926
- */
927
- async resume() {
928
- if (this.enabled) {
929
- this.status = "active";
930
- this.calculateNextRun();
931
- }
932
- await this.save();
933
- }
934
- /**
935
- * Calculate the next run time based on cron expression
936
- */
937
- calculateNextRun() {
938
- if (!this.cron || !this.enabled) {
939
- this.nextRun = null;
940
- return;
941
- }
942
- try {
943
- const next = getNextCronDate(this.cron, this.timezone);
944
- this.nextRun = next;
945
- } catch {
946
- this.nextRun = null;
947
- this.status = "error";
948
- this.lastError = `Invalid cron expression: ${this.cron}`;
949
- }
950
- }
951
- /**
952
- * Get a human-readable description of the schedule
953
- */
954
- getDescription() {
955
- const displayAgentType = getAgentClassName(this.agentType);
956
- const agent = this.agentId ? `${displayAgentType}#${this.agentId}` : displayAgentType;
957
- return `${agent}.${this.method}() @ ${this.cron}`;
958
- }
959
- /**
960
- * Lifecycle hook - calculate next run on save
961
- */
962
- async beforeSave() {
963
- if (this.agentType) {
964
- this.agentType = getAgentTypeName(this.agentType);
965
- }
966
- if (!this.nextRun && this.enabled) {
967
- this.calculateNextRun();
968
- }
969
- }
827
+ var AgentSchedule = class extends SmrtObject {
828
+ tenantId = null;
829
+ agentType = "";
830
+ agentId = null;
831
+ agentConfig = {};
832
+ cron = "";
833
+ timezone = "UTC";
834
+ enabled = true;
835
+ status = "active";
836
+ lastRun = null;
837
+ nextRun = null;
838
+ lastStatus = null;
839
+ lastError = null;
840
+ runCount = 0;
841
+ successCount = 0;
842
+ failureCount = 0;
843
+ maxConcurrent = 1;
844
+ runningCount = 0;
845
+ timeout = 36e5;
846
+ method = "run";
847
+ methodArgs = {};
848
+ /**
849
+ * Enable the schedule
850
+ */
851
+ async enable() {
852
+ this.enabled = true;
853
+ this.status = "active";
854
+ this.calculateNextRun();
855
+ await this.save();
856
+ }
857
+ /**
858
+ * Disable the schedule
859
+ */
860
+ async disable() {
861
+ this.enabled = false;
862
+ this.status = "disabled";
863
+ await this.save();
864
+ }
865
+ /**
866
+ * Pause the schedule temporarily
867
+ */
868
+ async pause() {
869
+ this.status = "paused";
870
+ await this.save();
871
+ }
872
+ /**
873
+ * Resume a paused schedule
874
+ */
875
+ async resume() {
876
+ if (this.enabled) {
877
+ this.status = "active";
878
+ this.calculateNextRun();
879
+ }
880
+ await this.save();
881
+ }
882
+ /**
883
+ * Calculate the next run time based on cron expression
884
+ */
885
+ calculateNextRun() {
886
+ if (!this.cron || !this.enabled) {
887
+ this.nextRun = null;
888
+ return;
889
+ }
890
+ try {
891
+ const next = getNextCronDate(this.cron, this.timezone);
892
+ this.nextRun = next;
893
+ } catch {
894
+ this.nextRun = null;
895
+ this.status = "error";
896
+ this.lastError = `Invalid cron expression: ${this.cron}`;
897
+ }
898
+ }
899
+ /**
900
+ * Get a human-readable description of the schedule
901
+ */
902
+ getDescription() {
903
+ const displayAgentType = getAgentClassName(this.agentType);
904
+ return `${this.agentId ? `${displayAgentType}#${this.agentId}` : displayAgentType}.${this.method}() @ ${this.cron}`;
905
+ }
906
+ /**
907
+ * Lifecycle hook - calculate next run on save
908
+ */
909
+ async beforeSave() {
910
+ if (this.agentType) this.agentType = getAgentTypeName(this.agentType);
911
+ if (!this.nextRun && this.enabled) this.calculateNextRun();
912
+ }
913
+ };
914
+ __decorateClass$1([tenantId({ nullable: true })], AgentSchedule.prototype, "tenantId", 2);
915
+ __decorateClass$1([field({ type: "text" })], AgentSchedule.prototype, "agentType", 2);
916
+ __decorateClass$1([field({
917
+ type: "text",
918
+ nullable: true
919
+ })], AgentSchedule.prototype, "agentId", 2);
920
+ __decorateClass$1([field({
921
+ type: "json",
922
+ sqlType: "TEXT",
923
+ sensitive: true
924
+ })], AgentSchedule.prototype, "agentConfig", 2);
925
+ __decorateClass$1([field({ type: "text" })], AgentSchedule.prototype, "cron", 2);
926
+ __decorateClass$1([field({ type: "text" })], AgentSchedule.prototype, "timezone", 2);
927
+ __decorateClass$1([field({ type: "boolean" })], AgentSchedule.prototype, "enabled", 2);
928
+ __decorateClass$1([field({ type: "text" })], AgentSchedule.prototype, "status", 2);
929
+ __decorateClass$1([field({
930
+ type: "datetime",
931
+ nullable: true
932
+ })], AgentSchedule.prototype, "lastRun", 2);
933
+ __decorateClass$1([field({
934
+ type: "datetime",
935
+ nullable: true
936
+ })], AgentSchedule.prototype, "nextRun", 2);
937
+ __decorateClass$1([field({
938
+ type: "text",
939
+ nullable: true
940
+ })], AgentSchedule.prototype, "lastStatus", 2);
941
+ __decorateClass$1([field({
942
+ type: "text",
943
+ nullable: true
944
+ })], AgentSchedule.prototype, "lastError", 2);
945
+ __decorateClass$1([field({ type: "integer" })], AgentSchedule.prototype, "runCount", 2);
946
+ __decorateClass$1([field({ type: "integer" })], AgentSchedule.prototype, "successCount", 2);
947
+ __decorateClass$1([field({ type: "integer" })], AgentSchedule.prototype, "failureCount", 2);
948
+ __decorateClass$1([field({ type: "integer" })], AgentSchedule.prototype, "maxConcurrent", 2);
949
+ __decorateClass$1([field({ type: "integer" })], AgentSchedule.prototype, "runningCount", 2);
950
+ __decorateClass$1([field({ type: "integer" })], AgentSchedule.prototype, "timeout", 2);
951
+ __decorateClass$1([field({ type: "text" })], AgentSchedule.prototype, "method", 2);
952
+ __decorateClass$1([field({
953
+ type: "json",
954
+ sqlType: "TEXT"
955
+ })], AgentSchedule.prototype, "methodArgs", 2);
956
+ AgentSchedule = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
957
+ tableName: "_smrt_agent_schedules",
958
+ api: { include: [
959
+ "list",
960
+ "get",
961
+ "create",
962
+ "update",
963
+ "delete"
964
+ ] },
965
+ cli: {
966
+ include: [
967
+ "list",
968
+ "get",
969
+ "create",
970
+ "update",
971
+ "delete",
972
+ "enable",
973
+ "disable"
974
+ ],
975
+ skipApiCheck: true
976
+ },
977
+ mcp: { include: ["list", "get"] }
978
+ })], AgentSchedule);
979
+ var AgentScheduleCollection = class extends SmrtCollection {
980
+ static _itemClass = AgentSchedule;
981
+ /**
982
+ * Find all schedules for a specific tenant
983
+ * @param tenantId - Tenant ID to filter by
984
+ * @returns Array of AgentSchedule objects for the tenant
985
+ */
986
+ async findByTenant(tenantId2) {
987
+ return this.list({ where: { tenantId: tenantId2 } });
988
+ }
989
+ /**
990
+ * Find all global schedules (not associated with any tenant).
991
+ *
992
+ * Routes through the shared tenant-global helper so it does not throw under
993
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
994
+ * flagged as an isolation violation). (#1600)
995
+ *
996
+ * @returns Array of global AgentSchedule objects
997
+ */
998
+ async findGlobal() {
999
+ return queryGlobal(this);
1000
+ }
1001
+ /**
1002
+ * Find schedules for a tenant including global schedules.
1003
+ *
1004
+ * Fails closed if an active tenant context requests a different tenant's
1005
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1006
+ *
1007
+ * @param tenantId - Tenant ID to include
1008
+ * @returns Array of AgentSchedule objects for the tenant and global schedules
1009
+ */
1010
+ async findWithGlobals(tenantId2) {
1011
+ return queryWithGlobals(this, tenantId2, "AgentSchedule.findWithGlobals");
1012
+ }
1013
+ /**
1014
+ * List schedules by status
1015
+ */
1016
+ async listByStatus(status, options = {}) {
1017
+ return this.list({
1018
+ where: { status: Array.isArray(status) ? status : [status] },
1019
+ orderBy: "next_run ASC",
1020
+ limit: options.limit
1021
+ });
1022
+ }
1023
+ /**
1024
+ * List schedules for a specific agent type
1025
+ */
1026
+ async listByAgentType(agentType, options = {}) {
1027
+ const aliases = getAgentTypeAliases(agentType);
1028
+ const where = aliases.length > 1 ? { "agentType in": aliases } : { agentType: getAgentTypeName(agentType) };
1029
+ if (!options.includeDisabled) where.enabled = true;
1030
+ return this.list({
1031
+ where,
1032
+ orderBy: "next_run ASC",
1033
+ limit: options.limit
1034
+ });
1035
+ }
970
1036
  };
971
- __decorateClass$1([
972
- tenantId({ nullable: true })
973
- ], AgentSchedule.prototype, "tenantId", 2);
974
- __decorateClass$1([
975
- field({ type: "text" })
976
- ], AgentSchedule.prototype, "agentType", 2);
977
- __decorateClass$1([
978
- field({ type: "text", nullable: true })
979
- ], AgentSchedule.prototype, "agentId", 2);
980
- __decorateClass$1([
981
- field({ type: "json", sqlType: "TEXT", sensitive: true })
982
- ], AgentSchedule.prototype, "agentConfig", 2);
983
- __decorateClass$1([
984
- field({ type: "text" })
985
- ], AgentSchedule.prototype, "cron", 2);
986
- __decorateClass$1([
987
- field({ type: "text" })
988
- ], AgentSchedule.prototype, "timezone", 2);
989
- __decorateClass$1([
990
- field({ type: "boolean" })
991
- ], AgentSchedule.prototype, "enabled", 2);
992
- __decorateClass$1([
993
- field({ type: "text" })
994
- ], AgentSchedule.prototype, "status", 2);
995
- __decorateClass$1([
996
- field({ type: "datetime", nullable: true })
997
- ], AgentSchedule.prototype, "lastRun", 2);
998
- __decorateClass$1([
999
- field({ type: "datetime", nullable: true })
1000
- ], AgentSchedule.prototype, "nextRun", 2);
1001
- __decorateClass$1([
1002
- field({ type: "text", nullable: true })
1003
- ], AgentSchedule.prototype, "lastStatus", 2);
1004
- __decorateClass$1([
1005
- field({ type: "text", nullable: true })
1006
- ], AgentSchedule.prototype, "lastError", 2);
1007
- __decorateClass$1([
1008
- field({ type: "integer" })
1009
- ], AgentSchedule.prototype, "runCount", 2);
1010
- __decorateClass$1([
1011
- field({ type: "integer" })
1012
- ], AgentSchedule.prototype, "successCount", 2);
1013
- __decorateClass$1([
1014
- field({ type: "integer" })
1015
- ], AgentSchedule.prototype, "failureCount", 2);
1016
- __decorateClass$1([
1017
- field({ type: "integer" })
1018
- ], AgentSchedule.prototype, "maxConcurrent", 2);
1019
- __decorateClass$1([
1020
- field({ type: "integer" })
1021
- ], AgentSchedule.prototype, "runningCount", 2);
1022
- __decorateClass$1([
1023
- field({ type: "integer" })
1024
- ], AgentSchedule.prototype, "timeout", 2);
1025
- __decorateClass$1([
1026
- field({ type: "text" })
1027
- ], AgentSchedule.prototype, "method", 2);
1028
- __decorateClass$1([
1029
- field({ type: "json", sqlType: "TEXT" })
1030
- ], AgentSchedule.prototype, "methodArgs", 2);
1031
- AgentSchedule = __decorateClass$1([
1032
- TenantScoped({ mode: "optional" }),
1033
- smrt({
1034
- tableName: "_smrt_agent_schedules",
1035
- api: { include: ["list", "get", "create", "update", "delete"] },
1036
- cli: {
1037
- include: ["list", "get", "create", "update", "delete", "enable", "disable"],
1038
- // enable/disable are operator commands invoked in-process via the CLI;
1039
- // they intentionally aren't exposed over HTTP.
1040
- skipApiCheck: true
1041
- },
1042
- mcp: { include: ["list", "get"] }
1043
- })
1044
- ], AgentSchedule);
1045
- class AgentScheduleCollection extends SmrtCollection {
1046
- static _itemClass = AgentSchedule;
1047
- /**
1048
- * Find all schedules for a specific tenant
1049
- * @param tenantId - Tenant ID to filter by
1050
- * @returns Array of AgentSchedule objects for the tenant
1051
- */
1052
- async findByTenant(tenantId2) {
1053
- return this.list({ where: { tenantId: tenantId2 } });
1054
- }
1055
- /**
1056
- * Find all global schedules (not associated with any tenant).
1057
- *
1058
- * Routes through the shared tenant-global helper so it does not throw under
1059
- * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1060
- * flagged as an isolation violation). (#1600)
1061
- *
1062
- * @returns Array of global AgentSchedule objects
1063
- */
1064
- async findGlobal() {
1065
- return queryGlobal(this);
1066
- }
1067
- /**
1068
- * Find schedules for a tenant including global schedules.
1069
- *
1070
- * Fails closed if an active tenant context requests a different tenant's
1071
- * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1072
- *
1073
- * @param tenantId - Tenant ID to include
1074
- * @returns Array of AgentSchedule objects for the tenant and global schedules
1075
- */
1076
- async findWithGlobals(tenantId2) {
1077
- return queryWithGlobals(
1078
- this,
1079
- tenantId2,
1080
- "AgentSchedule.findWithGlobals"
1081
- );
1082
- }
1083
- /**
1084
- * List schedules by status
1085
- */
1086
- async listByStatus(status, options = {}) {
1087
- return this.list({
1088
- where: {
1089
- status: Array.isArray(status) ? status : [status]
1090
- },
1091
- orderBy: "next_run ASC",
1092
- limit: options.limit
1093
- });
1094
- }
1095
- /**
1096
- * List schedules for a specific agent type
1097
- */
1098
- async listByAgentType(agentType, options = {}) {
1099
- const aliases = getAgentTypeAliases(agentType);
1100
- const where = aliases.length > 1 ? { "agentType in": aliases } : { agentType: getAgentTypeName(agentType) };
1101
- if (!options.includeDisabled) {
1102
- where.enabled = true;
1103
- }
1104
- return this.list({
1105
- where,
1106
- orderBy: "next_run ASC",
1107
- limit: options.limit
1108
- });
1109
- }
1110
- }
1111
1037
  function getNextCronDate(cron, _timezone = "UTC") {
1112
- const parts = cron.trim().split(/\s+/);
1113
- if (parts.length !== 5) {
1114
- throw new Error(
1115
- `Invalid cron expression: expected 5 fields, got ${parts.length}`
1116
- );
1117
- }
1118
- const [minuteExpr, hourExpr, dayExpr, monthExpr, dowExpr] = parts;
1119
- const now = /* @__PURE__ */ new Date();
1120
- const candidate = new Date(now);
1121
- candidate.setSeconds(0);
1122
- candidate.setMilliseconds(0);
1123
- candidate.setMinutes(candidate.getMinutes() + 1);
1124
- const dayIsWildcard = dayExpr === "*";
1125
- const dowIsWildcard = dowExpr === "*";
1126
- const maxIterations = 525600;
1127
- for (let i = 0; i < maxIterations; i++) {
1128
- const dayMatches = matchesCronField(candidate.getDate(), dayExpr);
1129
- const dow = candidate.getDay();
1130
- const dowMatches = matchesCronField(dow, dowExpr) || dow === 0 && matchesCronField(7, dowExpr);
1131
- let dayOfMonthOrWeekMatches;
1132
- if (!dayIsWildcard && !dowIsWildcard) {
1133
- dayOfMonthOrWeekMatches = dayMatches || dowMatches;
1134
- } else if (!dayIsWildcard) {
1135
- dayOfMonthOrWeekMatches = dayMatches;
1136
- } else if (!dowIsWildcard) {
1137
- dayOfMonthOrWeekMatches = dowMatches;
1138
- } else {
1139
- dayOfMonthOrWeekMatches = true;
1140
- }
1141
- if (matchesCronField(candidate.getMonth() + 1, monthExpr) && dayOfMonthOrWeekMatches && matchesCronField(candidate.getHours(), hourExpr) && matchesCronField(candidate.getMinutes(), minuteExpr)) {
1142
- return candidate;
1143
- }
1144
- candidate.setMinutes(candidate.getMinutes() + 1);
1145
- }
1146
- throw new Error(`Could not find next run date for cron: ${cron}`);
1038
+ const parts = cron.trim().split(/\s+/);
1039
+ if (parts.length !== 5) throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);
1040
+ const [minuteExpr, hourExpr, dayExpr, monthExpr, dowExpr] = parts;
1041
+ const candidate = /* @__PURE__ */ new Date(/* @__PURE__ */ new Date());
1042
+ candidate.setSeconds(0);
1043
+ candidate.setMilliseconds(0);
1044
+ candidate.setMinutes(candidate.getMinutes() + 1);
1045
+ const dayIsWildcard = dayExpr === "*";
1046
+ const dowIsWildcard = dowExpr === "*";
1047
+ const maxIterations = 525600;
1048
+ for (let i = 0; i < maxIterations; i++) {
1049
+ const dayMatches = matchesCronField(candidate.getDate(), dayExpr);
1050
+ const dow = candidate.getDay();
1051
+ const dowMatches = matchesCronField(dow, dowExpr) || dow === 0 && matchesCronField(7, dowExpr);
1052
+ let dayOfMonthOrWeekMatches;
1053
+ if (!dayIsWildcard && !dowIsWildcard) dayOfMonthOrWeekMatches = dayMatches || dowMatches;
1054
+ else if (!dayIsWildcard) dayOfMonthOrWeekMatches = dayMatches;
1055
+ else if (!dowIsWildcard) dayOfMonthOrWeekMatches = dowMatches;
1056
+ else dayOfMonthOrWeekMatches = true;
1057
+ if (matchesCronField(candidate.getMonth() + 1, monthExpr) && dayOfMonthOrWeekMatches && matchesCronField(candidate.getHours(), hourExpr) && matchesCronField(candidate.getMinutes(), minuteExpr)) return candidate;
1058
+ candidate.setMinutes(candidate.getMinutes() + 1);
1059
+ }
1060
+ throw new Error(`Could not find next run date for cron: ${cron}`);
1147
1061
  }
1148
1062
  function matchesCronField(value, expr) {
1149
- if (expr === "*") {
1150
- return true;
1151
- }
1152
- if (expr.includes("/")) {
1153
- const [range, stepStr] = expr.split("/");
1154
- const step = parseInt(stepStr, 10);
1155
- if (range === "*") {
1156
- return value % step === 0;
1157
- }
1158
- if (range.includes("-")) {
1159
- const [startStr, endStr] = range.split("-");
1160
- const start = parseInt(startStr, 10);
1161
- const end = parseInt(endStr, 10);
1162
- if (value < start || value > end) return false;
1163
- return (value - start) % step === 0;
1164
- }
1165
- }
1166
- if (expr.includes("-")) {
1167
- const [startStr, endStr] = expr.split("-");
1168
- const start = parseInt(startStr, 10);
1169
- const end = parseInt(endStr, 10);
1170
- return value >= start && value <= end;
1171
- }
1172
- if (expr.includes(",")) {
1173
- const values = expr.split(",").map((v) => parseInt(v.trim(), 10));
1174
- return values.includes(value);
1175
- }
1176
- return value === parseInt(expr, 10);
1063
+ if (expr === "*") return true;
1064
+ if (expr.includes("/")) {
1065
+ const [range, stepStr] = expr.split("/");
1066
+ const step = parseInt(stepStr, 10);
1067
+ if (range === "*") return value % step === 0;
1068
+ if (range.includes("-")) {
1069
+ const [startStr, endStr] = range.split("-");
1070
+ const start = parseInt(startStr, 10);
1071
+ if (value < start || value > parseInt(endStr, 10)) return false;
1072
+ return (value - start) % step === 0;
1073
+ }
1074
+ }
1075
+ if (expr.includes("-")) {
1076
+ const [startStr, endStr] = expr.split("-");
1077
+ return value >= parseInt(startStr, 10) && value <= parseInt(endStr, 10);
1078
+ }
1079
+ if (expr.includes(",")) return expr.split(",").map((v) => parseInt(v.trim(), 10)).includes(value);
1080
+ return value === parseInt(expr, 10);
1177
1081
  }
1082
+ //#endregion
1083
+ //#region src/tenant-agent.ts
1178
1084
  var __defProp = Object.defineProperty;
1179
1085
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1180
1086
  var __decorateClass = (decorators, target, key, kind) => {
1181
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1182
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
1183
- if (decorator = decorators[i])
1184
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1185
- if (kind && result) __defProp(target, key, result);
1186
- return result;
1087
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1088
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1089
+ if (kind && result) __defProp(target, key, result);
1090
+ return result;
1187
1091
  };
1188
- let TenantAgent = class extends SmrtObject {
1189
- tenantId = "";
1190
- agentClass = "";
1191
- status = "active";
1192
- permissions = null;
1193
- config = null;
1092
+ var TenantAgent = class extends SmrtObject {
1093
+ tenantId = "";
1094
+ agentClass = "";
1095
+ status = "active";
1096
+ permissions = null;
1097
+ config = null;
1194
1098
  };
1195
- __decorateClass([
1196
- tenantId()
1197
- ], TenantAgent.prototype, "tenantId", 2);
1198
- __decorateClass([
1199
- field({ type: "text" })
1200
- ], TenantAgent.prototype, "agentClass", 2);
1201
- __decorateClass([
1202
- field({ type: "text" })
1203
- ], TenantAgent.prototype, "status", 2);
1204
- __decorateClass([
1205
- field({ type: "json", nullable: true })
1206
- ], TenantAgent.prototype, "permissions", 2);
1207
- __decorateClass([
1208
- field({ type: "json", nullable: true, sensitive: true })
1209
- ], TenantAgent.prototype, "config", 2);
1210
- TenantAgent = __decorateClass([
1211
- TenantScoped({ mode: "required" }),
1212
- smrt({
1213
- tableName: "tenant_agents",
1214
- api: { include: ["list", "get", "create", "update", "delete"] },
1215
- cli: { include: ["list", "get"] },
1216
- mcp: { include: ["list", "get"] },
1217
- conflictColumns: ["tenant_id", "agent_class"]
1218
- })
1219
- ], TenantAgent);
1220
- class TenantAgentCollection extends SmrtCollection {
1221
- static _itemClass = TenantAgent;
1222
- /**
1223
- * Resolve agent availability for a tenant, walking up the hierarchy.
1224
- *
1225
- * Algorithm:
1226
- * 1. Load explicit entries for this tenant
1227
- * 2. Build result map from explicit entries (source = 'explicit')
1228
- * 3. Merge permissions: manifest defaults overridden by explicit permissions
1229
- * 4. Get tenant's ancestors via hierarchyPath (immediate parent → root)
1230
- * 5. For each ancestor, add inherited agents not already resolved
1231
- * 6. Return only agents that appear somewhere in the hierarchy
1232
- *
1233
- * @param tenantId - The tenant to resolve for
1234
- * @param getAncestorIds - Function that returns ancestor tenant IDs (parent → root order)
1235
- * @param manifests - Map of agent class name to AgentManifestInfo
1236
- */
1237
- async resolveForTenant(tenantId2, getAncestorIds, manifests) {
1238
- const result = /* @__PURE__ */ new Map();
1239
- const explicitEntries = await this.list({
1240
- where: { tenantId: tenantId2 }
1241
- });
1242
- for (const entry of explicitEntries) {
1243
- const agentType = await this.normalizeStoredAgentClass(entry);
1244
- const manifest = getManifestForAgent(manifests, agentType);
1245
- const mergedPermissions = mergePermissions(
1246
- manifest?.permissions,
1247
- entry.permissions
1248
- );
1249
- result.set(agentType, {
1250
- agentClass: getAgentClassName(agentType),
1251
- agentType,
1252
- status: entry.status,
1253
- source: "explicit",
1254
- sourceTenantId: tenantId2,
1255
- permissions: mergedPermissions,
1256
- manifest,
1257
- config: entry.config ?? void 0
1258
- });
1259
- }
1260
- const ancestorIds = await getAncestorIds(tenantId2);
1261
- for (const ancestorId of ancestorIds) {
1262
- const ancestorEntries = await this.list({
1263
- where: { tenantId: ancestorId }
1264
- });
1265
- for (const entry of ancestorEntries) {
1266
- const agentType = await this.normalizeStoredAgentClass(entry);
1267
- if (result.has(agentType)) continue;
1268
- const manifest = getManifestForAgent(manifests, agentType);
1269
- const mergedPermissions = mergePermissions(
1270
- manifest?.permissions,
1271
- entry.permissions
1272
- );
1273
- result.set(agentType, {
1274
- agentClass: getAgentClassName(agentType),
1275
- agentType,
1276
- status: entry.status,
1277
- source: "inherited",
1278
- sourceTenantId: ancestorId,
1279
- permissions: mergedPermissions,
1280
- manifest,
1281
- config: entry.config ?? void 0
1282
- });
1283
- }
1284
- }
1285
- return Array.from(result.values());
1286
- }
1287
- /**
1288
- * Enable an agent for a tenant (creates or updates binding)
1289
- */
1290
- async enableAgent(tenantId2, agentClass) {
1291
- const canonicalAgentClass = getAgentTypeName(agentClass);
1292
- const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1293
- if (existing) {
1294
- existing.status = "active";
1295
- await existing.save();
1296
- return existing;
1297
- }
1298
- const entry = await this.create({
1299
- tenantId: tenantId2,
1300
- agentClass: canonicalAgentClass,
1301
- status: "active"
1302
- });
1303
- await entry.save();
1304
- return entry;
1305
- }
1306
- /**
1307
- * Disable an agent for a tenant
1308
- */
1309
- async disableAgent(tenantId2, agentClass) {
1310
- const canonicalAgentClass = getAgentTypeName(agentClass);
1311
- const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1312
- if (existing) {
1313
- existing.status = "disabled";
1314
- await existing.save();
1315
- return existing;
1316
- }
1317
- const entry = await this.create({
1318
- tenantId: tenantId2,
1319
- agentClass: canonicalAgentClass,
1320
- status: "disabled"
1321
- });
1322
- await entry.save();
1323
- return entry;
1324
- }
1325
- /**
1326
- * Remove explicit override, falling back to inheritance
1327
- */
1328
- async clearOverride(tenantId2, agentClass) {
1329
- const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1330
- if (existing) {
1331
- await existing.delete();
1332
- }
1333
- }
1334
- /**
1335
- * Set permission overrides for a tenant's agent binding
1336
- */
1337
- async setPermissions(tenantId2, agentClass, permissions) {
1338
- const canonicalAgentClass = getAgentTypeName(agentClass);
1339
- const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1340
- if (existing) {
1341
- existing.permissions = permissions;
1342
- await existing.save();
1343
- return existing;
1344
- }
1345
- const entry = await this.create({
1346
- tenantId: tenantId2,
1347
- agentClass: canonicalAgentClass,
1348
- status: "active",
1349
- permissions
1350
- });
1351
- await entry.save();
1352
- return entry;
1353
- }
1354
- /**
1355
- * Find a tenant-agent binding by tenant and agent class
1356
- */
1357
- async findByTenantAndClass(tenantId2, agentClass) {
1358
- const aliases = getAgentTypeAliases(agentClass);
1359
- const results = await this.list({
1360
- where: aliases.length > 1 ? { tenantId: tenantId2, "agentClass in": aliases } : { tenantId: tenantId2, agentClass: aliases[0] }
1361
- });
1362
- const canonicalAgentClass = getAgentTypeName(agentClass);
1363
- const found = results.find((entry) => entry.agentClass === canonicalAgentClass) || results[0] || null;
1364
- if (found && found.agentClass !== canonicalAgentClass) {
1365
- await this.persistCanonicalAgentClass(found, canonicalAgentClass);
1366
- }
1367
- return found;
1368
- }
1369
- async normalizeStoredAgentClass(entry) {
1370
- const canonicalAgentClass = getAgentTypeName(entry.agentClass);
1371
- if (entry.agentClass !== canonicalAgentClass) {
1372
- await this.persistCanonicalAgentClass(entry, canonicalAgentClass);
1373
- }
1374
- return canonicalAgentClass;
1375
- }
1376
- async persistCanonicalAgentClass(entry, canonicalAgentClass) {
1377
- if (!entry.id || entry.agentClass === canonicalAgentClass) {
1378
- entry.agentClass = canonicalAgentClass;
1379
- return;
1380
- }
1381
- await this._db.query(
1382
- `UPDATE ${this.tableName}
1099
+ __decorateClass([tenantId()], TenantAgent.prototype, "tenantId", 2);
1100
+ __decorateClass([field({ type: "text" })], TenantAgent.prototype, "agentClass", 2);
1101
+ __decorateClass([field({ type: "text" })], TenantAgent.prototype, "status", 2);
1102
+ __decorateClass([field({
1103
+ type: "json",
1104
+ nullable: true
1105
+ })], TenantAgent.prototype, "permissions", 2);
1106
+ __decorateClass([field({
1107
+ type: "json",
1108
+ nullable: true,
1109
+ sensitive: true
1110
+ })], TenantAgent.prototype, "config", 2);
1111
+ TenantAgent = __decorateClass([TenantScoped({ mode: "required" }), smrt({
1112
+ tableName: "tenant_agents",
1113
+ api: { include: [
1114
+ "list",
1115
+ "get",
1116
+ "create",
1117
+ "update",
1118
+ "delete"
1119
+ ] },
1120
+ cli: { include: ["list", "get"] },
1121
+ mcp: { include: ["list", "get"] },
1122
+ conflictColumns: ["tenant_id", "agent_class"]
1123
+ })], TenantAgent);
1124
+ var TenantAgentCollection = class extends SmrtCollection {
1125
+ static _itemClass = TenantAgent;
1126
+ /**
1127
+ * Resolve agent availability for a tenant, walking up the hierarchy.
1128
+ *
1129
+ * Algorithm:
1130
+ * 1. Load explicit entries for this tenant
1131
+ * 2. Build result map from explicit entries (source = 'explicit')
1132
+ * 3. Merge permissions: manifest defaults overridden by explicit permissions
1133
+ * 4. Get tenant's ancestors via hierarchyPath (immediate parent → root)
1134
+ * 5. For each ancestor, add inherited agents not already resolved
1135
+ * 6. Return only agents that appear somewhere in the hierarchy
1136
+ *
1137
+ * @param tenantId - The tenant to resolve for
1138
+ * @param getAncestorIds - Function that returns ancestor tenant IDs (parent → root order)
1139
+ * @param manifests - Map of agent class name to AgentManifestInfo
1140
+ */
1141
+ async resolveForTenant(tenantId2, getAncestorIds, manifests) {
1142
+ const result = /* @__PURE__ */ new Map();
1143
+ const explicitEntries = await this.list({ where: { tenantId: tenantId2 } });
1144
+ for (const entry of explicitEntries) {
1145
+ const agentType = await this.normalizeStoredAgentClass(entry);
1146
+ const manifest = getManifestForAgent(manifests, agentType);
1147
+ const mergedPermissions = mergePermissions(manifest?.permissions, entry.permissions);
1148
+ result.set(agentType, {
1149
+ agentClass: getAgentClassName(agentType),
1150
+ agentType,
1151
+ status: entry.status,
1152
+ source: "explicit",
1153
+ sourceTenantId: tenantId2,
1154
+ permissions: mergedPermissions,
1155
+ manifest,
1156
+ config: entry.config ?? void 0
1157
+ });
1158
+ }
1159
+ const ancestorIds = await getAncestorIds(tenantId2);
1160
+ for (const ancestorId of ancestorIds) {
1161
+ const ancestorEntries = await this.list({ where: { tenantId: ancestorId } });
1162
+ for (const entry of ancestorEntries) {
1163
+ const agentType = await this.normalizeStoredAgentClass(entry);
1164
+ if (result.has(agentType)) continue;
1165
+ const manifest = getManifestForAgent(manifests, agentType);
1166
+ const mergedPermissions = mergePermissions(manifest?.permissions, entry.permissions);
1167
+ result.set(agentType, {
1168
+ agentClass: getAgentClassName(agentType),
1169
+ agentType,
1170
+ status: entry.status,
1171
+ source: "inherited",
1172
+ sourceTenantId: ancestorId,
1173
+ permissions: mergedPermissions,
1174
+ manifest,
1175
+ config: entry.config ?? void 0
1176
+ });
1177
+ }
1178
+ }
1179
+ return Array.from(result.values());
1180
+ }
1181
+ /**
1182
+ * Enable an agent for a tenant (creates or updates binding)
1183
+ */
1184
+ async enableAgent(tenantId2, agentClass) {
1185
+ const canonicalAgentClass = getAgentTypeName(agentClass);
1186
+ const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1187
+ if (existing) {
1188
+ existing.status = "active";
1189
+ await existing.save();
1190
+ return existing;
1191
+ }
1192
+ const entry = await this.create({
1193
+ tenantId: tenantId2,
1194
+ agentClass: canonicalAgentClass,
1195
+ status: "active"
1196
+ });
1197
+ await entry.save();
1198
+ return entry;
1199
+ }
1200
+ /**
1201
+ * Disable an agent for a tenant
1202
+ */
1203
+ async disableAgent(tenantId2, agentClass) {
1204
+ const canonicalAgentClass = getAgentTypeName(agentClass);
1205
+ const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1206
+ if (existing) {
1207
+ existing.status = "disabled";
1208
+ await existing.save();
1209
+ return existing;
1210
+ }
1211
+ const entry = await this.create({
1212
+ tenantId: tenantId2,
1213
+ agentClass: canonicalAgentClass,
1214
+ status: "disabled"
1215
+ });
1216
+ await entry.save();
1217
+ return entry;
1218
+ }
1219
+ /**
1220
+ * Remove explicit override, falling back to inheritance
1221
+ */
1222
+ async clearOverride(tenantId2, agentClass) {
1223
+ const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1224
+ if (existing) await existing.delete();
1225
+ }
1226
+ /**
1227
+ * Set permission overrides for a tenant's agent binding
1228
+ */
1229
+ async setPermissions(tenantId2, agentClass, permissions) {
1230
+ const canonicalAgentClass = getAgentTypeName(agentClass);
1231
+ const existing = await this.findByTenantAndClass(tenantId2, agentClass);
1232
+ if (existing) {
1233
+ existing.permissions = permissions;
1234
+ await existing.save();
1235
+ return existing;
1236
+ }
1237
+ const entry = await this.create({
1238
+ tenantId: tenantId2,
1239
+ agentClass: canonicalAgentClass,
1240
+ status: "active",
1241
+ permissions
1242
+ });
1243
+ await entry.save();
1244
+ return entry;
1245
+ }
1246
+ /**
1247
+ * Find a tenant-agent binding by tenant and agent class
1248
+ */
1249
+ async findByTenantAndClass(tenantId2, agentClass) {
1250
+ const aliases = getAgentTypeAliases(agentClass);
1251
+ const results = await this.list({ where: aliases.length > 1 ? {
1252
+ tenantId: tenantId2,
1253
+ "agentClass in": aliases
1254
+ } : {
1255
+ tenantId: tenantId2,
1256
+ agentClass: aliases[0]
1257
+ } });
1258
+ const canonicalAgentClass = getAgentTypeName(agentClass);
1259
+ const found = results.find((entry) => entry.agentClass === canonicalAgentClass) || results[0] || null;
1260
+ if (found && found.agentClass !== canonicalAgentClass) await this.persistCanonicalAgentClass(found, canonicalAgentClass);
1261
+ return found;
1262
+ }
1263
+ async normalizeStoredAgentClass(entry) {
1264
+ const canonicalAgentClass = getAgentTypeName(entry.agentClass);
1265
+ if (entry.agentClass !== canonicalAgentClass) await this.persistCanonicalAgentClass(entry, canonicalAgentClass);
1266
+ return canonicalAgentClass;
1267
+ }
1268
+ async persistCanonicalAgentClass(entry, canonicalAgentClass) {
1269
+ if (!entry.id || entry.agentClass === canonicalAgentClass) {
1270
+ entry.agentClass = canonicalAgentClass;
1271
+ return;
1272
+ }
1273
+ await this._db.query(`UPDATE ${this.tableName}
1383
1274
  SET agent_class = ?,
1384
1275
  updated_at = ?
1385
- WHERE id = ?`,
1386
- canonicalAgentClass,
1387
- (/* @__PURE__ */ new Date()).toISOString(),
1388
- entry.id
1389
- );
1390
- entry.agentClass = canonicalAgentClass;
1391
- }
1392
- }
1276
+ WHERE id = ?`, canonicalAgentClass, (/* @__PURE__ */ new Date()).toISOString(), entry.id);
1277
+ entry.agentClass = canonicalAgentClass;
1278
+ }
1279
+ };
1393
1280
  function mergePermissions(manifestPermissions, overrides) {
1394
- const result = {};
1395
- if (manifestPermissions) {
1396
- for (const perm of manifestPermissions) {
1397
- result[perm.id] = perm.defaultGranted !== false;
1398
- }
1399
- }
1400
- if (overrides) {
1401
- for (const [key, value] of Object.entries(overrides)) {
1402
- result[key] = value;
1403
- }
1404
- }
1405
- return result;
1281
+ const result = {};
1282
+ if (manifestPermissions) for (const perm of manifestPermissions) result[perm.id] = perm.defaultGranted !== false;
1283
+ if (overrides) for (const [key, value] of Object.entries(overrides)) result[key] = value;
1284
+ return result;
1406
1285
  }
1407
1286
  function getManifestForAgent(manifests, agentTypeOrIdentifier) {
1408
- if (!manifests) {
1409
- return void 0;
1410
- }
1411
- return manifests.get(agentTypeOrIdentifier) || manifests.get(getAgentClassName(agentTypeOrIdentifier));
1287
+ if (!manifests) return;
1288
+ return manifests.get(agentTypeOrIdentifier) || manifests.get(getAgentClassName(agentTypeOrIdentifier));
1412
1289
  }
1413
- export {
1414
- Agent,
1415
- AgentConfig,
1416
- c as AgentConfigCollection,
1417
- AgentSchedule,
1418
- AgentScheduleCollection,
1419
- AgentUIRegistry,
1420
- TenantAgent,
1421
- TenantAgentCollection,
1422
- createUIRegistry,
1423
- getClassConfigResolvers,
1424
- getConfigResolver,
1425
- isLazyConfigSentinel,
1426
- listConfigResolvers,
1427
- mergeFilters,
1428
- normalizeSort,
1429
- registerConfigResolver,
1430
- resetConfigResolvers,
1431
- resolveAgentAIOptions,
1432
- resolveLazyConfig,
1433
- unregisterConfigResolver
1434
- };
1435
- //# sourceMappingURL=index.js.map
1290
+ //#endregion
1291
+ export { Agent, AgentConfig, AgentConfigCollection, AgentSchedule, AgentScheduleCollection, AgentUIRegistry, TenantAgent, TenantAgentCollection, createUIRegistry, getClassConfigResolvers, getConfigResolver, isLazyConfigSentinel, listConfigResolvers, mergeFilters, normalizeSort, registerConfigResolver, resetConfigResolvers, resolveAgentAIOptions, resolveLazyConfig, unregisterConfigResolver };
1292
+
1293
+ //# sourceMappingURL=index.js.map