@happyvertical/smrt-agents 0.30.0
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/AGENTS.md +96 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +139 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/agent.d.ts +545 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/ai-config.d.ts +27 -0
- package/dist/ai-config.d.ts.map +1 -0
- package/dist/chunks/config-BYbOxt24.js +179 -0
- package/dist/chunks/config-BYbOxt24.js.map +1 -0
- package/dist/chunks/manifest-utils-DLXfTOq0.js +69 -0
- package/dist/chunks/manifest-utils-DLXfTOq0.js.map +1 -0
- package/dist/config.d.ts +117 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/identity.d.ts +19 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1477 -0
- package/dist/index.js.map +1 -0
- package/dist/interests.d.ts +291 -0
- package/dist/interests.d.ts.map +1 -0
- package/dist/manifest.json +2012 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +156 -0
- package/dist/playground.js.map +1 -0
- package/dist/schedule.d.ts +168 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/server/action-types.d.ts +65 -0
- package/dist/server/action-types.d.ts.map +1 -0
- package/dist/server/action-types.js +2 -0
- package/dist/server/action-types.js.map +1 -0
- package/dist/server/api-routes.d.ts +57 -0
- package/dist/server/api-routes.d.ts.map +1 -0
- package/dist/server/config-loader.d.ts +17 -0
- package/dist/server/config-loader.d.ts.map +1 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/manifest-utils.d.ts +63 -0
- package/dist/server/manifest-utils.d.ts.map +1 -0
- package/dist/server/serialization.d.ts +58 -0
- package/dist/server/serialization.d.ts.map +1 -0
- package/dist/server.js +105 -0
- package/dist/server.js.map +1 -0
- package/dist/smrt-knowledge.json +983 -0
- package/dist/summary-article.d.ts +30 -0
- package/dist/summary-article.d.ts.map +1 -0
- package/dist/summary-article.js +2 -0
- package/dist/summary-article.js.map +1 -0
- package/dist/svelte/components/AgentDashboard.svelte +250 -0
- package/dist/svelte/components/AgentDashboard.svelte.d.ts +21 -0
- package/dist/svelte/components/AgentDashboard.svelte.d.ts.map +1 -0
- package/dist/svelte/components/AgentRunHistory.svelte +225 -0
- package/dist/svelte/components/AgentRunHistory.svelte.d.ts +16 -0
- package/dist/svelte/components/AgentRunHistory.svelte.d.ts.map +1 -0
- package/dist/svelte/components/AgentScheduleForm.svelte +381 -0
- package/dist/svelte/components/AgentScheduleForm.svelte.d.ts +19 -0
- package/dist/svelte/components/AgentScheduleForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/AgentScheduleList.svelte +370 -0
- package/dist/svelte/components/AgentScheduleList.svelte.d.ts +24 -0
- package/dist/svelte/components/AgentScheduleList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ScheduleStatusBadge.svelte +23 -0
- package/dist/svelte/components/ScheduleStatusBadge.svelte.d.ts +9 -0
- package/dist/svelte/components/ScheduleStatusBadge.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +33 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +37 -0
- package/dist/svelte/index.d.ts +23 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +26 -0
- package/dist/svelte/playground.d.ts +196 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +151 -0
- package/dist/svelte/types.d.ts +155 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +116 -0
- package/dist/tenant-agent.d.ts +106 -0
- package/dist/tenant-agent.d.ts.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +298 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +133 -0
- package/dist/ui.js.map +1 -0
- package/dist/vite-plugin.d.ts +61 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +173 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +104 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1477 @@
|
|
|
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";
|
|
3
|
+
import { createLogger } from "@happyvertical/logger";
|
|
4
|
+
import { sanitizeConfig } from "@happyvertical/smrt-config";
|
|
5
|
+
import { getCurrentTenant, withTenant, tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
|
|
6
|
+
import { SecretService } from "@happyvertical/smrt-secrets";
|
|
7
|
+
import { TenantCollection } from "@happyvertical/smrt-users";
|
|
8
|
+
import { g as getAgentTypeName, a as getAgentClassName, A as AgentConfig, b as getAgentTypeAliases } from "./chunks/config-BYbOxt24.js";
|
|
9
|
+
import { c } from "./chunks/config-BYbOxt24.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"
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_SECRET_FALLBACK = "ancestors";
|
|
20
|
+
const secretServiceCache = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
const tenantCollectionCache = /* @__PURE__ */ new WeakMap();
|
|
22
|
+
function asNonEmptyString(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
24
|
+
}
|
|
25
|
+
function normalizeSecretFallback(value) {
|
|
26
|
+
return value === "none" ? "none" : DEFAULT_SECRET_FALLBACK;
|
|
27
|
+
}
|
|
28
|
+
function getDefaultSecretName(aiConfig) {
|
|
29
|
+
const provider = asNonEmptyString(aiConfig.type)?.toLowerCase();
|
|
30
|
+
if (!provider) {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
return DEFAULT_SECRET_NAMES[provider];
|
|
34
|
+
}
|
|
35
|
+
function stripAgentAISecretFields(aiConfig) {
|
|
36
|
+
const {
|
|
37
|
+
apiKeySecretName: _apiKeySecretName,
|
|
38
|
+
apiKeySecretFallback: _apiKeySecretFallback,
|
|
39
|
+
...rest
|
|
40
|
+
} = aiConfig;
|
|
41
|
+
return rest;
|
|
42
|
+
}
|
|
43
|
+
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;
|
|
51
|
+
}
|
|
52
|
+
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;
|
|
60
|
+
}
|
|
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;
|
|
74
|
+
}
|
|
75
|
+
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;
|
|
92
|
+
}
|
|
93
|
+
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";
|
|
98
|
+
}
|
|
99
|
+
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
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function mergeFilters(globalFilter, objectFilter) {
|
|
129
|
+
if (!globalFilter && !objectFilter) return {};
|
|
130
|
+
if (!globalFilter) return { ...objectFilter };
|
|
131
|
+
if (!objectFilter) return { ...globalFilter };
|
|
132
|
+
return { ...globalFilter, ...objectFilter };
|
|
133
|
+
}
|
|
134
|
+
function normalizeSort(sort) {
|
|
135
|
+
if (!sort) return [];
|
|
136
|
+
return Array.isArray(sort) ? sort : [sort];
|
|
137
|
+
}
|
|
138
|
+
var __defProp$2 = Object.defineProperty;
|
|
139
|
+
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;
|
|
141
|
+
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;
|
|
148
|
+
};
|
|
149
|
+
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] = { ...merged[slotId], ...data };
|
|
322
|
+
}
|
|
323
|
+
if (!options?.includeSecrets) {
|
|
324
|
+
return sanitizeConfig(merged);
|
|
325
|
+
}
|
|
326
|
+
return merged;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get the DispatchBus for inter-agent communication
|
|
330
|
+
*
|
|
331
|
+
* Creates a DispatchBus lazily on first access. Requires database configuration.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```typescript
|
|
335
|
+
* // Emit a dispatch to other agents
|
|
336
|
+
* await this.dispatch.emit('campaign.completed', {
|
|
337
|
+
* campaignId: '123',
|
|
338
|
+
* revenue: 5000
|
|
339
|
+
* }, { source: this.constructor.name });
|
|
340
|
+
*
|
|
341
|
+
* // Subscribe to dispatches
|
|
342
|
+
* await this.dispatch.subscribe({
|
|
343
|
+
* signalType: 'campaign.*',
|
|
344
|
+
* subscriber: this.constructor.name
|
|
345
|
+
* });
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* @throws Error if database is not configured
|
|
349
|
+
*/
|
|
350
|
+
async getDispatch() {
|
|
351
|
+
if (!this._dispatch) {
|
|
352
|
+
if (!this._db) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Agent ${this.constructor.name} requires database configuration for dispatch. Ensure the agent is initialized with a db option.`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
this._dispatch = await createDispatchBus({
|
|
358
|
+
db: this._db
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return this._dispatch;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Handle incoming dispatches
|
|
365
|
+
*
|
|
366
|
+
* Override this method to process dispatches targeted at this agent.
|
|
367
|
+
* Called when process() is invoked for this agent's subscriber name.
|
|
368
|
+
*
|
|
369
|
+
* @param payload - Dispatch payload data
|
|
370
|
+
* @param metadata - Dispatch metadata including type, source, and timing
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* async handleDispatch(payload: unknown, metadata: DispatchMetadata): Promise<void> {
|
|
375
|
+
* if (metadata.type === 'campaign.completed') {
|
|
376
|
+
* const data = payload as { campaignId: string; revenue: number };
|
|
377
|
+
* await this.recordRevenue(data.campaignId, data.revenue);
|
|
378
|
+
* }
|
|
379
|
+
* }
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
async handleDispatch(_payload, _metadata) {
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Process pending dispatches for this agent
|
|
386
|
+
*
|
|
387
|
+
* Finds and processes all pending dispatches that match this agent's subscriptions.
|
|
388
|
+
* Uses handleDispatch() to process each dispatch.
|
|
389
|
+
*
|
|
390
|
+
* @returns Number of dispatches processed
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```typescript
|
|
394
|
+
* // In your run() method
|
|
395
|
+
* const processed = await this.processDispatches();
|
|
396
|
+
* this.logger.info(`Processed ${processed} dispatches`);
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
async processDispatches() {
|
|
400
|
+
const dispatch = await this.getDispatch();
|
|
401
|
+
return dispatch.process(
|
|
402
|
+
this.getAgentTypeName(),
|
|
403
|
+
this.handleDispatch.bind(this)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Initialize the agent
|
|
408
|
+
* Sets status to 'initializing' and sets up signal handlers
|
|
409
|
+
*
|
|
410
|
+
* Override to perform setup after construction, but always call super.initialize()
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* async initialize(): Promise<void> {
|
|
415
|
+
* await super.initialize();
|
|
416
|
+
* // Custom initialization logic
|
|
417
|
+
* }
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
async initialize() {
|
|
421
|
+
await super.initialize();
|
|
422
|
+
this.status = "initializing";
|
|
423
|
+
this.logger.info("Agent initializing");
|
|
424
|
+
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;
|
|
425
|
+
const configuredAi = this.options.ai ?? fileAiConfig;
|
|
426
|
+
if (configuredAi && this._db) {
|
|
427
|
+
const resolvedAi = await resolveAgentAIOptions({
|
|
428
|
+
aiConfig: configuredAi,
|
|
429
|
+
db: this._db,
|
|
430
|
+
tenantId: getCurrentTenant()?.tenantId || (typeof this.tenantId === "string" ? this.tenantId : void 0)
|
|
431
|
+
});
|
|
432
|
+
if (resolvedAi) {
|
|
433
|
+
this.options.ai = resolvedAi;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (this.options.manageProcessSignals) {
|
|
437
|
+
this.setupSignalHandlers();
|
|
438
|
+
}
|
|
439
|
+
if (this._db) {
|
|
440
|
+
const dispatch = await this.getDispatch();
|
|
441
|
+
await this.migrateLegacyDispatchSubscriptions(dispatch);
|
|
442
|
+
const subs = this.constructor.signalSubscriptions;
|
|
443
|
+
if (subs.length > 0) {
|
|
444
|
+
const subscriber = this.getAgentTypeName();
|
|
445
|
+
const existing = await dispatch.listSubscriptions(subscriber);
|
|
446
|
+
const existingTypes = new Set(existing.map((s) => s.signalType));
|
|
447
|
+
for (const signalType of subs) {
|
|
448
|
+
if (!existingTypes.has(signalType)) {
|
|
449
|
+
await dispatch.subscribe({
|
|
450
|
+
signalType,
|
|
451
|
+
subscriber
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Set up signal handlers for graceful shutdown
|
|
461
|
+
* Handles SIGTERM and SIGINT for single-agent processes that explicitly opt in.
|
|
462
|
+
*/
|
|
463
|
+
setupSignalHandlers() {
|
|
464
|
+
const signals = ["SIGTERM", "SIGINT"];
|
|
465
|
+
for (const signal of signals) {
|
|
466
|
+
const handler = () => {
|
|
467
|
+
this.logger.info(`Received ${signal}, shutting down gracefully`);
|
|
468
|
+
this.shutdown().then(() => {
|
|
469
|
+
process.exit(0);
|
|
470
|
+
}).catch((error) => {
|
|
471
|
+
this.logger.error("Error during shutdown", { error });
|
|
472
|
+
process.exit(1);
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
this.signalHandlers.set(signal, handler);
|
|
476
|
+
process.on(signal, handler);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Migrate legacy simple-name dispatch subscribers to the canonical agent type.
|
|
481
|
+
*
|
|
482
|
+
* Older releases used `this.constructor.name` directly for subscriber IDs.
|
|
483
|
+
* That collides across packages and leaves fan-out dispatches targeted at the
|
|
484
|
+
* wrong subscriber once qualified names are available.
|
|
485
|
+
*/
|
|
486
|
+
async migrateLegacyDispatchSubscriptions(dispatch) {
|
|
487
|
+
if (!this._db) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const legacySubscriber = this.constructor.name;
|
|
491
|
+
const canonicalSubscriber = this.getAgentTypeName();
|
|
492
|
+
if (legacySubscriber === canonicalSubscriber) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const legacySubscriptions = await dispatch.listSubscriptions(legacySubscriber);
|
|
496
|
+
if (legacySubscriptions.length === 0) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const currentSubscriptions = await dispatch.listSubscriptions(canonicalSubscriber);
|
|
500
|
+
const currentSignalTypes = new Set(
|
|
501
|
+
currentSubscriptions.map((sub) => sub.signalType)
|
|
502
|
+
);
|
|
503
|
+
for (const subscription of legacySubscriptions) {
|
|
504
|
+
if (!currentSignalTypes.has(subscription.signalType)) {
|
|
505
|
+
await dispatch.subscribe({
|
|
506
|
+
signalType: subscription.signalType,
|
|
507
|
+
subscriber: canonicalSubscriber,
|
|
508
|
+
handler: subscription.handler,
|
|
509
|
+
delivery: subscription.delivery,
|
|
510
|
+
enabled: subscription.enabled
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
await dispatch.unsubscribe(subscription.signalType, legacySubscriber);
|
|
514
|
+
}
|
|
515
|
+
const [tenantClause, tenantParams] = buildDispatchTenantUpdatePredicate(
|
|
516
|
+
resolveDispatchTenantScope()
|
|
517
|
+
);
|
|
518
|
+
await this._db.query(
|
|
519
|
+
`UPDATE _smrt_dispatch
|
|
520
|
+
SET target_subscriber = CASE
|
|
521
|
+
WHEN target_subscriber = ? THEN ?
|
|
522
|
+
ELSE target_subscriber
|
|
523
|
+
END,
|
|
524
|
+
processed_by = CASE
|
|
525
|
+
WHEN processed_by = ? THEN ?
|
|
526
|
+
ELSE processed_by
|
|
527
|
+
END
|
|
528
|
+
WHERE (target_subscriber = ? OR processed_by = ?)${tenantClause}`,
|
|
529
|
+
legacySubscriber,
|
|
530
|
+
canonicalSubscriber,
|
|
531
|
+
legacySubscriber,
|
|
532
|
+
canonicalSubscriber,
|
|
533
|
+
legacySubscriber,
|
|
534
|
+
legacySubscriber,
|
|
535
|
+
...tenantParams
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Clean up signal handlers
|
|
540
|
+
*/
|
|
541
|
+
cleanupSignalHandlers() {
|
|
542
|
+
for (const [signal, handler] of this.signalHandlers.entries()) {
|
|
543
|
+
process.removeListener(signal, handler);
|
|
544
|
+
}
|
|
545
|
+
this.signalHandlers.clear();
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Validate configuration and dependencies
|
|
549
|
+
* Override to check agent-specific requirements
|
|
550
|
+
*
|
|
551
|
+
* @throws Error if validation fails
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* ```typescript
|
|
555
|
+
* async validate(): Promise<void> {
|
|
556
|
+
* if (!this.config.apiKey) {
|
|
557
|
+
* throw new Error('API key is required');
|
|
558
|
+
* }
|
|
559
|
+
* }
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
async validate() {
|
|
563
|
+
this.logger.info("Validating agent configuration");
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Cleanup and shutdown
|
|
567
|
+
* Override to perform graceful shutdown
|
|
568
|
+
*
|
|
569
|
+
* Always call super.shutdown() to clean up signal handlers
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* ```typescript
|
|
573
|
+
* async shutdown(): Promise<void> {
|
|
574
|
+
* this.logger.info('Cleaning up resources');
|
|
575
|
+
* await this.cleanup();
|
|
576
|
+
* await super.shutdown();
|
|
577
|
+
* }
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
async shutdown() {
|
|
581
|
+
this.status = "shutdown";
|
|
582
|
+
this.logger.info("Agent shutting down");
|
|
583
|
+
this.cleanupSignalHandlers();
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Execute agent with lifecycle management
|
|
587
|
+
*
|
|
588
|
+
* Runs the full lifecycle:
|
|
589
|
+
* 1. initialize() — seeds signal subscriptions if declared
|
|
590
|
+
* 2. validate()
|
|
591
|
+
* 3. processDispatches() — auto-processes pending dispatches if subscriptions exist
|
|
592
|
+
* 4. run()
|
|
593
|
+
*
|
|
594
|
+
* Note: handleDispatch() callbacks may fire before run() is entered.
|
|
595
|
+
*
|
|
596
|
+
* On error:
|
|
597
|
+
* 1. Sets status to 'error'
|
|
598
|
+
* 2. Logs error
|
|
599
|
+
* 3. Re-throws error
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```typescript
|
|
603
|
+
* const agent = new MyAgent({ name: 'my-agent' });
|
|
604
|
+
*
|
|
605
|
+
* try {
|
|
606
|
+
* await agent.execute();
|
|
607
|
+
* console.log('Agent completed successfully');
|
|
608
|
+
* } catch (error) {
|
|
609
|
+
* console.error('Agent failed:', error);
|
|
610
|
+
* }
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
async execute() {
|
|
614
|
+
try {
|
|
615
|
+
await this.initialize();
|
|
616
|
+
await this.validate();
|
|
617
|
+
this.status = "running";
|
|
618
|
+
if (this._db) {
|
|
619
|
+
const dispatch = await this.getDispatch();
|
|
620
|
+
const subs = await dispatch.listSubscriptions(this.getAgentTypeName());
|
|
621
|
+
if (subs.length > 0) {
|
|
622
|
+
const count = await this.processDispatches();
|
|
623
|
+
if (count > 0) {
|
|
624
|
+
this.logger.info(`Processed ${count} pending dispatches`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
await this.run();
|
|
629
|
+
this.status = "idle";
|
|
630
|
+
this.logger.info("Agent execution completed");
|
|
631
|
+
} catch (error) {
|
|
632
|
+
this.status = "error";
|
|
633
|
+
this.logger.error("Agent execution failed", { error });
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Query objects this agent is interested in
|
|
639
|
+
*
|
|
640
|
+
* Returns items from all configured object types, filtered and sorted
|
|
641
|
+
* according to interest configuration. If handlers are defined on filters,
|
|
642
|
+
* they are called for each matched item and the result is included.
|
|
643
|
+
*
|
|
644
|
+
* @returns Array of { type, data, name?, handled? } results
|
|
645
|
+
* @throws Error if no interests are configured
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* const items = await this.interesting();
|
|
650
|
+
* for (const { type, data, name, handled } of items) {
|
|
651
|
+
* console.log(`Processing ${type} from "${name}": action=${handled?.action}`);
|
|
652
|
+
* }
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
async interesting() {
|
|
656
|
+
if (!this.interests) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`Agent ${this.constructor.name} has no interests configured. Set interests in constructor options to use interesting().`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
if (!this.interests.objects || Object.keys(this.interests.objects).length === 0) {
|
|
662
|
+
this.logger.warn("Agent has empty interests.objects configuration");
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
const results = [];
|
|
666
|
+
for (const [className, config] of Object.entries(this.interests.objects)) {
|
|
667
|
+
try {
|
|
668
|
+
const items = await this.queryInterestingObjects(className, config);
|
|
669
|
+
results.push(...items);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
this.logger.warn(`Failed to query ${className} for interests`, {
|
|
672
|
+
error
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (this.interests.qualify) {
|
|
677
|
+
const allItems = results.map((r) => r.data);
|
|
678
|
+
const qualified = await this.interests.qualify(allItems);
|
|
679
|
+
const qualifiedSet = new Set(qualified);
|
|
680
|
+
const filteredResults = results.filter((r) => qualifiedSet.has(r.data));
|
|
681
|
+
if (this.interests.sort) {
|
|
682
|
+
return this.sortResults(filteredResults, this.interests.sort);
|
|
683
|
+
}
|
|
684
|
+
return filteredResults;
|
|
685
|
+
}
|
|
686
|
+
if (this.interests.sort) {
|
|
687
|
+
return this.sortResults(results, this.interests.sort);
|
|
688
|
+
}
|
|
689
|
+
return results;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Query a single object type based on interest config
|
|
693
|
+
*
|
|
694
|
+
* Supports both single filter and array of filters.
|
|
695
|
+
* Each filter can use standard SDK filters OR custom query function.
|
|
696
|
+
* Returns InterestResult[] with handler results included.
|
|
697
|
+
*/
|
|
698
|
+
async queryInterestingObjects(className, config) {
|
|
699
|
+
if (!ObjectRegistry.hasClass(className)) {
|
|
700
|
+
this.logger.warn(
|
|
701
|
+
`Object type "${className}" not found in ObjectRegistry. Skipping in interests query.`
|
|
702
|
+
);
|
|
703
|
+
return [];
|
|
704
|
+
}
|
|
705
|
+
const collection = await ObjectRegistry.getCollection(
|
|
706
|
+
className,
|
|
707
|
+
this.options
|
|
708
|
+
);
|
|
709
|
+
const filters = this.normalizeInterestConfig(config);
|
|
710
|
+
const allResults = [];
|
|
711
|
+
for (const filter of filters) {
|
|
712
|
+
const items = await this.queryInterestFilter(
|
|
713
|
+
className,
|
|
714
|
+
filter,
|
|
715
|
+
collection
|
|
716
|
+
);
|
|
717
|
+
for (const item of items) {
|
|
718
|
+
const result = {
|
|
719
|
+
type: className,
|
|
720
|
+
data: item,
|
|
721
|
+
name: filter.name
|
|
722
|
+
};
|
|
723
|
+
if (filter.handler) {
|
|
724
|
+
result.handled = await filter.handler(item, this);
|
|
725
|
+
}
|
|
726
|
+
allResults.push(result);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return allResults;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Normalize ObjectInterestConfig to array format
|
|
733
|
+
*/
|
|
734
|
+
normalizeInterestConfig(config) {
|
|
735
|
+
return Array.isArray(config) ? config : [config];
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Query a single interest filter
|
|
739
|
+
*
|
|
740
|
+
* Uses collection.query() for custom query functions,
|
|
741
|
+
* or collection.list() for standard SDK filters.
|
|
742
|
+
*/
|
|
743
|
+
async queryInterestFilter(_className, filter, collection) {
|
|
744
|
+
if (filter.query) {
|
|
745
|
+
let [whereClause, params] = filter.query(collection.tableName);
|
|
746
|
+
await ObjectRegistry.ensureManifestLoaded(_className);
|
|
747
|
+
let currentClass = ObjectRegistry.getClass(_className);
|
|
748
|
+
while (currentClass?.extends) {
|
|
749
|
+
const parentName = currentClass.extends;
|
|
750
|
+
if (parentName === "SmrtObject" || parentName === "SmrtClass" || parentName === "SmrtCollection") {
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
await ObjectRegistry.ensureManifestLoaded(parentName);
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
currentClass = ObjectRegistry.getClass(parentName);
|
|
758
|
+
}
|
|
759
|
+
ObjectRegistry.invalidateInheritanceCache(_className);
|
|
760
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(_className);
|
|
761
|
+
if (tableStrategy === "sti") {
|
|
762
|
+
const stiBase = ObjectRegistry.getSTIBase(_className);
|
|
763
|
+
const classInfo = ObjectRegistry.getClass(_className);
|
|
764
|
+
const qualifiedClassName = classInfo?.qualifiedName ?? classInfo?.name ?? _className;
|
|
765
|
+
if (stiBase && stiBase !== qualifiedClassName && stiBase !== _className) {
|
|
766
|
+
const metaTypeValue = classInfo?.qualifiedName || _className;
|
|
767
|
+
whereClause = `_meta_type = ? AND (${whereClause})`;
|
|
768
|
+
params = [metaTypeValue, ...params];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
let sql = `SELECT * FROM ${collection.tableName} WHERE ${whereClause}`;
|
|
772
|
+
if (filter.sort) {
|
|
773
|
+
const sorts = Array.isArray(filter.sort) ? filter.sort : [filter.sort];
|
|
774
|
+
sql += ` ORDER BY ${sorts.join(", ")}`;
|
|
775
|
+
}
|
|
776
|
+
if (filter.limit) {
|
|
777
|
+
sql += ` LIMIT ?`;
|
|
778
|
+
params.push(filter.limit);
|
|
779
|
+
}
|
|
780
|
+
let items2 = await collection.query(sql, params);
|
|
781
|
+
if (filter.qualify) {
|
|
782
|
+
items2 = await filter.qualify(items2);
|
|
783
|
+
}
|
|
784
|
+
return items2;
|
|
785
|
+
}
|
|
786
|
+
const mergedFilter = mergeFilters(this.interests?.filter, filter.filter);
|
|
787
|
+
const queryOptions = {};
|
|
788
|
+
if (Object.keys(mergedFilter).length > 0) {
|
|
789
|
+
queryOptions.where = mergedFilter;
|
|
790
|
+
}
|
|
791
|
+
if (filter.sort) {
|
|
792
|
+
queryOptions.orderBy = filter.sort;
|
|
793
|
+
}
|
|
794
|
+
if (filter.limit) {
|
|
795
|
+
queryOptions.limit = filter.limit;
|
|
796
|
+
}
|
|
797
|
+
let items = await collection.list(queryOptions);
|
|
798
|
+
if (filter.qualify) {
|
|
799
|
+
items = await filter.qualify(items);
|
|
800
|
+
}
|
|
801
|
+
return items;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Sort results by field(s) across all types
|
|
805
|
+
*/
|
|
806
|
+
sortResults(results, sort) {
|
|
807
|
+
const sortFields = normalizeSort(sort);
|
|
808
|
+
if (sortFields.length === 0) return results;
|
|
809
|
+
return [...results].sort((a, b) => {
|
|
810
|
+
for (const sortField of sortFields) {
|
|
811
|
+
const [field2, direction = "ASC"] = sortField.trim().split(/\s+/);
|
|
812
|
+
const aValue = a.data[field2];
|
|
813
|
+
const bValue = b.data[field2];
|
|
814
|
+
let comparison = 0;
|
|
815
|
+
if (aValue < bValue) comparison = -1;
|
|
816
|
+
else if (aValue > bValue) comparison = 1;
|
|
817
|
+
if (comparison !== 0) {
|
|
818
|
+
return direction.toUpperCase() === "DESC" ? -comparison : comparison;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return 0;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
__publicField(Agent, "uiSlots", {});
|
|
826
|
+
__publicField(Agent, "adminRoutes", []);
|
|
827
|
+
__publicField(Agent, "signalSubscriptions", []);
|
|
828
|
+
__publicField(Agent, "configResolvers", {});
|
|
829
|
+
__decorateClass$2([
|
|
830
|
+
tenantId({ nullable: true })
|
|
831
|
+
], Agent.prototype, "tenantId", 2);
|
|
832
|
+
Agent = __decorateClass$2([
|
|
833
|
+
TenantScoped({ mode: "optional" }),
|
|
834
|
+
smrt({
|
|
835
|
+
// Abstract class - no direct CLI/API/MCP exposure
|
|
836
|
+
// But must be registered for inheritance chain to work (issue #523)
|
|
837
|
+
cli: false,
|
|
838
|
+
api: false,
|
|
839
|
+
mcp: false,
|
|
840
|
+
// STI: All agents share 'agents' table for polymorphic queries
|
|
841
|
+
tableStrategy: "sti"
|
|
842
|
+
})
|
|
843
|
+
], Agent);
|
|
844
|
+
function buildDispatchTenantUpdatePredicate(scope) {
|
|
845
|
+
if (!scope.enforced) {
|
|
846
|
+
return ["", []];
|
|
847
|
+
}
|
|
848
|
+
if (scope.tenantId !== null) {
|
|
849
|
+
return [" AND (tenant_id = ? OR tenant_id IS NULL)", [scope.tenantId]];
|
|
850
|
+
}
|
|
851
|
+
return [" AND tenant_id IS NULL", []];
|
|
852
|
+
}
|
|
853
|
+
var __defProp$1 = Object.defineProperty;
|
|
854
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
855
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
856
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
857
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
858
|
+
if (decorator = decorators[i])
|
|
859
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
860
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
861
|
+
return result;
|
|
862
|
+
};
|
|
863
|
+
let AgentSchedule = class extends SmrtObject {
|
|
864
|
+
tenantId = null;
|
|
865
|
+
agentType = "";
|
|
866
|
+
agentId = null;
|
|
867
|
+
agentConfig = {};
|
|
868
|
+
cron = "";
|
|
869
|
+
timezone = "UTC";
|
|
870
|
+
enabled = true;
|
|
871
|
+
status = "active";
|
|
872
|
+
lastRun = null;
|
|
873
|
+
nextRun = null;
|
|
874
|
+
lastStatus = null;
|
|
875
|
+
lastError = null;
|
|
876
|
+
runCount = 0;
|
|
877
|
+
successCount = 0;
|
|
878
|
+
failureCount = 0;
|
|
879
|
+
maxConcurrent = 1;
|
|
880
|
+
runningCount = 0;
|
|
881
|
+
timeout = 36e5;
|
|
882
|
+
method = "run";
|
|
883
|
+
methodArgs = {};
|
|
884
|
+
/**
|
|
885
|
+
* Enable the schedule
|
|
886
|
+
*/
|
|
887
|
+
async enable() {
|
|
888
|
+
this.enabled = true;
|
|
889
|
+
this.status = "active";
|
|
890
|
+
this.calculateNextRun();
|
|
891
|
+
await this.save();
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Disable the schedule
|
|
895
|
+
*/
|
|
896
|
+
async disable() {
|
|
897
|
+
this.enabled = false;
|
|
898
|
+
this.status = "disabled";
|
|
899
|
+
await this.save();
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Pause the schedule temporarily
|
|
903
|
+
*/
|
|
904
|
+
async pause() {
|
|
905
|
+
this.status = "paused";
|
|
906
|
+
await this.save();
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Resume a paused schedule
|
|
910
|
+
*/
|
|
911
|
+
async resume() {
|
|
912
|
+
if (this.enabled) {
|
|
913
|
+
this.status = "active";
|
|
914
|
+
this.calculateNextRun();
|
|
915
|
+
}
|
|
916
|
+
await this.save();
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Record a successful run
|
|
920
|
+
*/
|
|
921
|
+
async recordSuccess() {
|
|
922
|
+
this.lastRun = /* @__PURE__ */ new Date();
|
|
923
|
+
this.lastStatus = "success";
|
|
924
|
+
this.lastError = null;
|
|
925
|
+
this.runCount++;
|
|
926
|
+
this.successCount++;
|
|
927
|
+
this.runningCount = Math.max(0, this.runningCount - 1);
|
|
928
|
+
this.calculateNextRun();
|
|
929
|
+
await this.save();
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Record a failed run
|
|
933
|
+
*/
|
|
934
|
+
async recordFailure(error) {
|
|
935
|
+
this.lastRun = /* @__PURE__ */ new Date();
|
|
936
|
+
this.lastStatus = "failed";
|
|
937
|
+
this.lastError = error;
|
|
938
|
+
this.runCount++;
|
|
939
|
+
this.failureCount++;
|
|
940
|
+
this.runningCount = Math.max(0, this.runningCount - 1);
|
|
941
|
+
this.calculateNextRun();
|
|
942
|
+
await this.save();
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Check if the schedule is due to run
|
|
946
|
+
*/
|
|
947
|
+
isDue() {
|
|
948
|
+
if (!this.enabled || this.status !== "active") {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (!this.nextRun) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
if (this.runningCount >= this.maxConcurrent) {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
return /* @__PURE__ */ new Date() >= this.nextRun;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Calculate the next run time based on cron expression
|
|
961
|
+
*/
|
|
962
|
+
calculateNextRun() {
|
|
963
|
+
if (!this.cron || !this.enabled) {
|
|
964
|
+
this.nextRun = null;
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const next = getNextCronDate(this.cron, this.timezone);
|
|
969
|
+
this.nextRun = next;
|
|
970
|
+
} catch {
|
|
971
|
+
this.nextRun = null;
|
|
972
|
+
this.status = "error";
|
|
973
|
+
this.lastError = `Invalid cron expression: ${this.cron}`;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Get a human-readable description of the schedule
|
|
978
|
+
*/
|
|
979
|
+
getDescription() {
|
|
980
|
+
const displayAgentType = getAgentClassName(this.agentType);
|
|
981
|
+
const agent = this.agentId ? `${displayAgentType}#${this.agentId}` : displayAgentType;
|
|
982
|
+
return `${agent}.${this.method}() @ ${this.cron}`;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Lifecycle hook - calculate next run on save
|
|
986
|
+
*/
|
|
987
|
+
async beforeSave() {
|
|
988
|
+
if (this.agentType) {
|
|
989
|
+
this.agentType = getAgentTypeName(this.agentType);
|
|
990
|
+
}
|
|
991
|
+
if (!this.nextRun && this.enabled) {
|
|
992
|
+
this.calculateNextRun();
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
__decorateClass$1([
|
|
997
|
+
tenantId({ nullable: true })
|
|
998
|
+
], AgentSchedule.prototype, "tenantId", 2);
|
|
999
|
+
__decorateClass$1([
|
|
1000
|
+
field({ type: "text" })
|
|
1001
|
+
], AgentSchedule.prototype, "agentType", 2);
|
|
1002
|
+
__decorateClass$1([
|
|
1003
|
+
field({ type: "text", nullable: true })
|
|
1004
|
+
], AgentSchedule.prototype, "agentId", 2);
|
|
1005
|
+
__decorateClass$1([
|
|
1006
|
+
field({ type: "json", sqlType: "TEXT", sensitive: true })
|
|
1007
|
+
], AgentSchedule.prototype, "agentConfig", 2);
|
|
1008
|
+
__decorateClass$1([
|
|
1009
|
+
field({ type: "text" })
|
|
1010
|
+
], AgentSchedule.prototype, "cron", 2);
|
|
1011
|
+
__decorateClass$1([
|
|
1012
|
+
field({ type: "text" })
|
|
1013
|
+
], AgentSchedule.prototype, "timezone", 2);
|
|
1014
|
+
__decorateClass$1([
|
|
1015
|
+
field({ type: "boolean" })
|
|
1016
|
+
], AgentSchedule.prototype, "enabled", 2);
|
|
1017
|
+
__decorateClass$1([
|
|
1018
|
+
field({ type: "text" })
|
|
1019
|
+
], AgentSchedule.prototype, "status", 2);
|
|
1020
|
+
__decorateClass$1([
|
|
1021
|
+
field({ type: "datetime", nullable: true })
|
|
1022
|
+
], AgentSchedule.prototype, "lastRun", 2);
|
|
1023
|
+
__decorateClass$1([
|
|
1024
|
+
field({ type: "datetime", nullable: true })
|
|
1025
|
+
], AgentSchedule.prototype, "nextRun", 2);
|
|
1026
|
+
__decorateClass$1([
|
|
1027
|
+
field({ type: "text", nullable: true })
|
|
1028
|
+
], AgentSchedule.prototype, "lastStatus", 2);
|
|
1029
|
+
__decorateClass$1([
|
|
1030
|
+
field({ type: "text", nullable: true })
|
|
1031
|
+
], AgentSchedule.prototype, "lastError", 2);
|
|
1032
|
+
__decorateClass$1([
|
|
1033
|
+
field({ type: "integer" })
|
|
1034
|
+
], AgentSchedule.prototype, "runCount", 2);
|
|
1035
|
+
__decorateClass$1([
|
|
1036
|
+
field({ type: "integer" })
|
|
1037
|
+
], AgentSchedule.prototype, "successCount", 2);
|
|
1038
|
+
__decorateClass$1([
|
|
1039
|
+
field({ type: "integer" })
|
|
1040
|
+
], AgentSchedule.prototype, "failureCount", 2);
|
|
1041
|
+
__decorateClass$1([
|
|
1042
|
+
field({ type: "integer" })
|
|
1043
|
+
], AgentSchedule.prototype, "maxConcurrent", 2);
|
|
1044
|
+
__decorateClass$1([
|
|
1045
|
+
field({ type: "integer" })
|
|
1046
|
+
], AgentSchedule.prototype, "runningCount", 2);
|
|
1047
|
+
__decorateClass$1([
|
|
1048
|
+
field({ type: "integer" })
|
|
1049
|
+
], AgentSchedule.prototype, "timeout", 2);
|
|
1050
|
+
__decorateClass$1([
|
|
1051
|
+
field({ type: "text" })
|
|
1052
|
+
], AgentSchedule.prototype, "method", 2);
|
|
1053
|
+
__decorateClass$1([
|
|
1054
|
+
field({ type: "json", sqlType: "TEXT" })
|
|
1055
|
+
], AgentSchedule.prototype, "methodArgs", 2);
|
|
1056
|
+
AgentSchedule = __decorateClass$1([
|
|
1057
|
+
TenantScoped({ mode: "optional" }),
|
|
1058
|
+
smrt({
|
|
1059
|
+
tableName: "_smrt_agent_schedules",
|
|
1060
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
1061
|
+
cli: {
|
|
1062
|
+
include: ["list", "get", "create", "update", "delete", "enable", "disable"],
|
|
1063
|
+
// enable/disable are operator commands invoked in-process via the CLI;
|
|
1064
|
+
// they intentionally aren't exposed over HTTP.
|
|
1065
|
+
skipApiCheck: true
|
|
1066
|
+
},
|
|
1067
|
+
mcp: { include: ["list", "get"] }
|
|
1068
|
+
})
|
|
1069
|
+
], AgentSchedule);
|
|
1070
|
+
class AgentScheduleCollection extends SmrtCollection {
|
|
1071
|
+
static _itemClass = AgentSchedule;
|
|
1072
|
+
/**
|
|
1073
|
+
* Find all schedules for a specific tenant
|
|
1074
|
+
* @param tenantId - Tenant ID to filter by
|
|
1075
|
+
* @returns Array of AgentSchedule objects for the tenant
|
|
1076
|
+
*/
|
|
1077
|
+
async findByTenant(tenantId2) {
|
|
1078
|
+
return this.list({ where: { tenantId: tenantId2 } });
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Find all global schedules (not associated with any tenant)
|
|
1082
|
+
* @returns Array of global AgentSchedule objects
|
|
1083
|
+
*/
|
|
1084
|
+
async findGlobal() {
|
|
1085
|
+
return this.list({ where: { tenantId: null } });
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Find schedules for a tenant including global schedules
|
|
1089
|
+
* @param tenantId - Tenant ID to include
|
|
1090
|
+
* @returns Array of AgentSchedule objects for the tenant and global schedules
|
|
1091
|
+
*/
|
|
1092
|
+
async findWithGlobals(tenantId2) {
|
|
1093
|
+
return this.query(
|
|
1094
|
+
`SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
|
|
1095
|
+
[tenantId2]
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* List schedules by status
|
|
1100
|
+
*/
|
|
1101
|
+
async listByStatus(status, options = {}) {
|
|
1102
|
+
return this.list({
|
|
1103
|
+
where: {
|
|
1104
|
+
status: Array.isArray(status) ? status : [status]
|
|
1105
|
+
},
|
|
1106
|
+
orderBy: "next_run ASC",
|
|
1107
|
+
limit: options.limit
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* List schedules that are due to run
|
|
1112
|
+
*/
|
|
1113
|
+
async listDue(options = {}) {
|
|
1114
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1115
|
+
return this.query(
|
|
1116
|
+
`SELECT * FROM _smrt_agent_schedules
|
|
1117
|
+
WHERE enabled = 1
|
|
1118
|
+
AND status = 'active'
|
|
1119
|
+
AND next_run <= ?
|
|
1120
|
+
AND running_count < max_concurrent
|
|
1121
|
+
ORDER BY next_run ASC
|
|
1122
|
+
LIMIT ?`,
|
|
1123
|
+
[now, options.limit || 100]
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* List schedules for a specific agent type
|
|
1128
|
+
*/
|
|
1129
|
+
async listByAgentType(agentType, options = {}) {
|
|
1130
|
+
const aliases = getAgentTypeAliases(agentType);
|
|
1131
|
+
const where = aliases.length > 1 ? { "agentType in": aliases } : { agentType: getAgentTypeName(agentType) };
|
|
1132
|
+
if (!options.includeDisabled) {
|
|
1133
|
+
where.enabled = true;
|
|
1134
|
+
}
|
|
1135
|
+
return this.list({
|
|
1136
|
+
where,
|
|
1137
|
+
orderBy: "next_run ASC",
|
|
1138
|
+
limit: options.limit
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Get schedule statistics
|
|
1143
|
+
*/
|
|
1144
|
+
async stats() {
|
|
1145
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1146
|
+
const result = await this._db.query(
|
|
1147
|
+
`SELECT
|
|
1148
|
+
COUNT(*) as total,
|
|
1149
|
+
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
|
1150
|
+
SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) as paused,
|
|
1151
|
+
SUM(CASE WHEN status = 'disabled' THEN 1 ELSE 0 END) as disabled,
|
|
1152
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
1153
|
+
SUM(CASE WHEN enabled = 1 AND status = 'active' AND next_run <= ? THEN 1 ELSE 0 END) as due_now
|
|
1154
|
+
FROM _smrt_agent_schedules`,
|
|
1155
|
+
[now]
|
|
1156
|
+
);
|
|
1157
|
+
const row = result.rows[0] || {};
|
|
1158
|
+
return {
|
|
1159
|
+
total: row.total ?? 0,
|
|
1160
|
+
active: row.active ?? 0,
|
|
1161
|
+
paused: row.paused ?? 0,
|
|
1162
|
+
disabled: row.disabled ?? 0,
|
|
1163
|
+
error: row.error ?? 0,
|
|
1164
|
+
dueNow: row.due_now ?? 0
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function getNextCronDate(cron, _timezone = "UTC") {
|
|
1169
|
+
const parts = cron.trim().split(/\s+/);
|
|
1170
|
+
if (parts.length !== 5) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`Invalid cron expression: expected 5 fields, got ${parts.length}`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
const [minuteExpr, hourExpr, dayExpr, monthExpr, dowExpr] = parts;
|
|
1176
|
+
const now = /* @__PURE__ */ new Date();
|
|
1177
|
+
const candidate = new Date(now);
|
|
1178
|
+
candidate.setSeconds(0);
|
|
1179
|
+
candidate.setMilliseconds(0);
|
|
1180
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
1181
|
+
const maxIterations = 525600;
|
|
1182
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
1183
|
+
if (matchesCronField(candidate.getMonth() + 1, monthExpr) && matchesCronField(candidate.getDate(), dayExpr) && matchesCronField(candidate.getDay(), dowExpr) && matchesCronField(candidate.getHours(), hourExpr) && matchesCronField(candidate.getMinutes(), minuteExpr)) {
|
|
1184
|
+
return candidate;
|
|
1185
|
+
}
|
|
1186
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
1187
|
+
}
|
|
1188
|
+
throw new Error(`Could not find next run date for cron: ${cron}`);
|
|
1189
|
+
}
|
|
1190
|
+
function matchesCronField(value, expr) {
|
|
1191
|
+
if (expr === "*") {
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
if (expr.includes("/")) {
|
|
1195
|
+
const [range, stepStr] = expr.split("/");
|
|
1196
|
+
const step = parseInt(stepStr, 10);
|
|
1197
|
+
if (range === "*") {
|
|
1198
|
+
return value % step === 0;
|
|
1199
|
+
}
|
|
1200
|
+
if (range.includes("-")) {
|
|
1201
|
+
const [startStr, endStr] = range.split("-");
|
|
1202
|
+
const start = parseInt(startStr, 10);
|
|
1203
|
+
const end = parseInt(endStr, 10);
|
|
1204
|
+
if (value < start || value > end) return false;
|
|
1205
|
+
return (value - start) % step === 0;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (expr.includes("-")) {
|
|
1209
|
+
const [startStr, endStr] = expr.split("-");
|
|
1210
|
+
const start = parseInt(startStr, 10);
|
|
1211
|
+
const end = parseInt(endStr, 10);
|
|
1212
|
+
return value >= start && value <= end;
|
|
1213
|
+
}
|
|
1214
|
+
if (expr.includes(",")) {
|
|
1215
|
+
const values = expr.split(",").map((v) => parseInt(v.trim(), 10));
|
|
1216
|
+
return values.includes(value);
|
|
1217
|
+
}
|
|
1218
|
+
return value === parseInt(expr, 10);
|
|
1219
|
+
}
|
|
1220
|
+
var __defProp = Object.defineProperty;
|
|
1221
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
1222
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
1223
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
1224
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
1225
|
+
if (decorator = decorators[i])
|
|
1226
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1227
|
+
if (kind && result) __defProp(target, key, result);
|
|
1228
|
+
return result;
|
|
1229
|
+
};
|
|
1230
|
+
let TenantAgent = class extends SmrtObject {
|
|
1231
|
+
tenantId = "";
|
|
1232
|
+
agentClass = "";
|
|
1233
|
+
status = "active";
|
|
1234
|
+
permissions = null;
|
|
1235
|
+
config = null;
|
|
1236
|
+
};
|
|
1237
|
+
__decorateClass([
|
|
1238
|
+
tenantId()
|
|
1239
|
+
], TenantAgent.prototype, "tenantId", 2);
|
|
1240
|
+
__decorateClass([
|
|
1241
|
+
field({ type: "text" })
|
|
1242
|
+
], TenantAgent.prototype, "agentClass", 2);
|
|
1243
|
+
__decorateClass([
|
|
1244
|
+
field({ type: "text" })
|
|
1245
|
+
], TenantAgent.prototype, "status", 2);
|
|
1246
|
+
__decorateClass([
|
|
1247
|
+
field({ type: "json", nullable: true })
|
|
1248
|
+
], TenantAgent.prototype, "permissions", 2);
|
|
1249
|
+
__decorateClass([
|
|
1250
|
+
field({ type: "json", nullable: true, sensitive: true })
|
|
1251
|
+
], TenantAgent.prototype, "config", 2);
|
|
1252
|
+
TenantAgent = __decorateClass([
|
|
1253
|
+
TenantScoped({ mode: "required" }),
|
|
1254
|
+
smrt({
|
|
1255
|
+
tableName: "tenant_agents",
|
|
1256
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
1257
|
+
cli: { include: ["list", "get"] },
|
|
1258
|
+
mcp: { include: ["list", "get"] },
|
|
1259
|
+
conflictColumns: ["tenant_id", "agent_class"]
|
|
1260
|
+
})
|
|
1261
|
+
], TenantAgent);
|
|
1262
|
+
class TenantAgentCollection extends SmrtCollection {
|
|
1263
|
+
static _itemClass = TenantAgent;
|
|
1264
|
+
/**
|
|
1265
|
+
* Resolve agent availability for a tenant, walking up the hierarchy.
|
|
1266
|
+
*
|
|
1267
|
+
* Algorithm:
|
|
1268
|
+
* 1. Load explicit entries for this tenant
|
|
1269
|
+
* 2. Build result map from explicit entries (source = 'explicit')
|
|
1270
|
+
* 3. Merge permissions: manifest defaults overridden by explicit permissions
|
|
1271
|
+
* 4. Get tenant's ancestors via hierarchyPath (immediate parent → root)
|
|
1272
|
+
* 5. For each ancestor, add inherited agents not already resolved
|
|
1273
|
+
* 6. Return only agents that appear somewhere in the hierarchy
|
|
1274
|
+
*
|
|
1275
|
+
* @param tenantId - The tenant to resolve for
|
|
1276
|
+
* @param getAncestorIds - Function that returns ancestor tenant IDs (parent → root order)
|
|
1277
|
+
* @param manifests - Map of agent class name to AgentManifestInfo
|
|
1278
|
+
*/
|
|
1279
|
+
async resolveForTenant(tenantId2, getAncestorIds, manifests) {
|
|
1280
|
+
const result = /* @__PURE__ */ new Map();
|
|
1281
|
+
const explicitEntries = await this.list({
|
|
1282
|
+
where: { tenantId: tenantId2 }
|
|
1283
|
+
});
|
|
1284
|
+
for (const entry of explicitEntries) {
|
|
1285
|
+
const agentType = await this.normalizeStoredAgentClass(entry);
|
|
1286
|
+
const manifest = getManifestForAgent(manifests, agentType);
|
|
1287
|
+
const mergedPermissions = mergePermissions(
|
|
1288
|
+
manifest?.permissions,
|
|
1289
|
+
entry.permissions
|
|
1290
|
+
);
|
|
1291
|
+
result.set(agentType, {
|
|
1292
|
+
agentClass: getAgentClassName(agentType),
|
|
1293
|
+
agentType,
|
|
1294
|
+
status: entry.status,
|
|
1295
|
+
source: "explicit",
|
|
1296
|
+
sourceTenantId: tenantId2,
|
|
1297
|
+
permissions: mergedPermissions,
|
|
1298
|
+
manifest,
|
|
1299
|
+
config: entry.config ?? void 0
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
const ancestorIds = await getAncestorIds(tenantId2);
|
|
1303
|
+
for (const ancestorId of ancestorIds) {
|
|
1304
|
+
const ancestorEntries = await this.list({
|
|
1305
|
+
where: { tenantId: ancestorId }
|
|
1306
|
+
});
|
|
1307
|
+
for (const entry of ancestorEntries) {
|
|
1308
|
+
const agentType = await this.normalizeStoredAgentClass(entry);
|
|
1309
|
+
if (result.has(agentType)) continue;
|
|
1310
|
+
const manifest = getManifestForAgent(manifests, agentType);
|
|
1311
|
+
const mergedPermissions = mergePermissions(
|
|
1312
|
+
manifest?.permissions,
|
|
1313
|
+
entry.permissions
|
|
1314
|
+
);
|
|
1315
|
+
result.set(agentType, {
|
|
1316
|
+
agentClass: getAgentClassName(agentType),
|
|
1317
|
+
agentType,
|
|
1318
|
+
status: entry.status,
|
|
1319
|
+
source: "inherited",
|
|
1320
|
+
sourceTenantId: ancestorId,
|
|
1321
|
+
permissions: mergedPermissions,
|
|
1322
|
+
manifest,
|
|
1323
|
+
config: entry.config ?? void 0
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return Array.from(result.values());
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Enable an agent for a tenant (creates or updates binding)
|
|
1331
|
+
*/
|
|
1332
|
+
async enableAgent(tenantId2, agentClass) {
|
|
1333
|
+
const canonicalAgentClass = getAgentTypeName(agentClass);
|
|
1334
|
+
const existing = await this.findByTenantAndClass(tenantId2, agentClass);
|
|
1335
|
+
if (existing) {
|
|
1336
|
+
existing.status = "active";
|
|
1337
|
+
await existing.save();
|
|
1338
|
+
return existing;
|
|
1339
|
+
}
|
|
1340
|
+
const entry = await this.create({
|
|
1341
|
+
tenantId: tenantId2,
|
|
1342
|
+
agentClass: canonicalAgentClass,
|
|
1343
|
+
status: "active"
|
|
1344
|
+
});
|
|
1345
|
+
await entry.save();
|
|
1346
|
+
return entry;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Disable an agent for a tenant
|
|
1350
|
+
*/
|
|
1351
|
+
async disableAgent(tenantId2, agentClass) {
|
|
1352
|
+
const canonicalAgentClass = getAgentTypeName(agentClass);
|
|
1353
|
+
const existing = await this.findByTenantAndClass(tenantId2, agentClass);
|
|
1354
|
+
if (existing) {
|
|
1355
|
+
existing.status = "disabled";
|
|
1356
|
+
await existing.save();
|
|
1357
|
+
return existing;
|
|
1358
|
+
}
|
|
1359
|
+
const entry = await this.create({
|
|
1360
|
+
tenantId: tenantId2,
|
|
1361
|
+
agentClass: canonicalAgentClass,
|
|
1362
|
+
status: "disabled"
|
|
1363
|
+
});
|
|
1364
|
+
await entry.save();
|
|
1365
|
+
return entry;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Remove explicit override, falling back to inheritance
|
|
1369
|
+
*/
|
|
1370
|
+
async clearOverride(tenantId2, agentClass) {
|
|
1371
|
+
const existing = await this.findByTenantAndClass(tenantId2, agentClass);
|
|
1372
|
+
if (existing) {
|
|
1373
|
+
await existing.delete();
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Set permission overrides for a tenant's agent binding
|
|
1378
|
+
*/
|
|
1379
|
+
async setPermissions(tenantId2, agentClass, permissions) {
|
|
1380
|
+
const canonicalAgentClass = getAgentTypeName(agentClass);
|
|
1381
|
+
const existing = await this.findByTenantAndClass(tenantId2, agentClass);
|
|
1382
|
+
if (existing) {
|
|
1383
|
+
existing.permissions = permissions;
|
|
1384
|
+
await existing.save();
|
|
1385
|
+
return existing;
|
|
1386
|
+
}
|
|
1387
|
+
const entry = await this.create({
|
|
1388
|
+
tenantId: tenantId2,
|
|
1389
|
+
agentClass: canonicalAgentClass,
|
|
1390
|
+
status: "active",
|
|
1391
|
+
permissions
|
|
1392
|
+
});
|
|
1393
|
+
await entry.save();
|
|
1394
|
+
return entry;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Find a tenant-agent binding by tenant and agent class
|
|
1398
|
+
*/
|
|
1399
|
+
async findByTenantAndClass(tenantId2, agentClass) {
|
|
1400
|
+
const aliases = getAgentTypeAliases(agentClass);
|
|
1401
|
+
const results = await this.list({
|
|
1402
|
+
where: aliases.length > 1 ? { tenantId: tenantId2, "agentClass in": aliases } : { tenantId: tenantId2, agentClass: aliases[0] }
|
|
1403
|
+
});
|
|
1404
|
+
const canonicalAgentClass = getAgentTypeName(agentClass);
|
|
1405
|
+
const found = results.find((entry) => entry.agentClass === canonicalAgentClass) || results[0] || null;
|
|
1406
|
+
if (found && found.agentClass !== canonicalAgentClass) {
|
|
1407
|
+
await this.persistCanonicalAgentClass(found, canonicalAgentClass);
|
|
1408
|
+
}
|
|
1409
|
+
return found;
|
|
1410
|
+
}
|
|
1411
|
+
async normalizeStoredAgentClass(entry) {
|
|
1412
|
+
const canonicalAgentClass = getAgentTypeName(entry.agentClass);
|
|
1413
|
+
if (entry.agentClass !== canonicalAgentClass) {
|
|
1414
|
+
await this.persistCanonicalAgentClass(entry, canonicalAgentClass);
|
|
1415
|
+
}
|
|
1416
|
+
return canonicalAgentClass;
|
|
1417
|
+
}
|
|
1418
|
+
async persistCanonicalAgentClass(entry, canonicalAgentClass) {
|
|
1419
|
+
if (!entry.id || entry.agentClass === canonicalAgentClass) {
|
|
1420
|
+
entry.agentClass = canonicalAgentClass;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
await this._db.query(
|
|
1424
|
+
`UPDATE ${this.tableName}
|
|
1425
|
+
SET agent_class = ?,
|
|
1426
|
+
updated_at = ?
|
|
1427
|
+
WHERE id = ?`,
|
|
1428
|
+
canonicalAgentClass,
|
|
1429
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1430
|
+
entry.id
|
|
1431
|
+
);
|
|
1432
|
+
entry.agentClass = canonicalAgentClass;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function mergePermissions(manifestPermissions, overrides) {
|
|
1436
|
+
const result = {};
|
|
1437
|
+
if (manifestPermissions) {
|
|
1438
|
+
for (const perm of manifestPermissions) {
|
|
1439
|
+
result[perm.id] = perm.defaultGranted !== false;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (overrides) {
|
|
1443
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
1444
|
+
result[key] = value;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return result;
|
|
1448
|
+
}
|
|
1449
|
+
function getManifestForAgent(manifests, agentTypeOrIdentifier) {
|
|
1450
|
+
if (!manifests) {
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
return manifests.get(agentTypeOrIdentifier) || manifests.get(getAgentClassName(agentTypeOrIdentifier));
|
|
1454
|
+
}
|
|
1455
|
+
export {
|
|
1456
|
+
Agent,
|
|
1457
|
+
AgentConfig,
|
|
1458
|
+
c as AgentConfigCollection,
|
|
1459
|
+
AgentSchedule,
|
|
1460
|
+
AgentScheduleCollection,
|
|
1461
|
+
AgentUIRegistry,
|
|
1462
|
+
TenantAgent,
|
|
1463
|
+
TenantAgentCollection,
|
|
1464
|
+
createUIRegistry,
|
|
1465
|
+
getClassConfigResolvers,
|
|
1466
|
+
getConfigResolver,
|
|
1467
|
+
isLazyConfigSentinel,
|
|
1468
|
+
listConfigResolvers,
|
|
1469
|
+
mergeFilters,
|
|
1470
|
+
normalizeSort,
|
|
1471
|
+
registerConfigResolver,
|
|
1472
|
+
resetConfigResolvers,
|
|
1473
|
+
resolveAgentAIOptions,
|
|
1474
|
+
resolveLazyConfig,
|
|
1475
|
+
unregisterConfigResolver
|
|
1476
|
+
};
|
|
1477
|
+
//# sourceMappingURL=index.js.map
|