@memberjunction/ai-agent-manager 2.107.0 → 2.108.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.
@@ -0,0 +1,744 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentSpecSync = void 0;
4
+ const core_1 = require("@memberjunction/core");
5
+ /**
6
+ * AgentSpecSync provides a high-level interface for working with AI Agent metadata in MemberJunction.
7
+ *
8
+ * This class serves as a bi-directional bridge between the simple, serializable {@link AgentSpec}
9
+ * format and the complex MemberJunction database metadata across three core entities:
10
+ * - {@link AIAgentEntity} - Core agent configuration
11
+ * - {@link AIAgentActionEntity} - Agent-action relationships
12
+ * - {@link AIAgentRelationshipEntity} - Parent-child agent relationships
13
+ *
14
+ * ## Key Features
15
+ *
16
+ * - **Load from Database**: Load complete agent hierarchies including all sub-agents and actions
17
+ * - **Save to Database**: Persist changes atomically with full validation
18
+ * - **Recursive Support**: Handles n-level agent hierarchies automatically
19
+ * - **Type Safety**: Full TypeScript typing with proper entity types
20
+ * - **Dirty Tracking**: Knows when changes need to be saved
21
+ * - **JSON Serialization**: Easy export for APIs and storage
22
+ *
23
+ * ## Usage Examples
24
+ *
25
+ * ### Load an existing agent
26
+ * ```typescript
27
+ * const spec = await AgentSpecSync.LoadFromDatabase('agent-uuid', contextUser);
28
+ * console.log('Agent Name:', spec.spec.Name);
29
+ * console.log('Actions:', spec.spec.Actions?.length);
30
+ * ```
31
+ *
32
+ * ### Create a new agent
33
+ * ```typescript
34
+ * const newAgent = new AgentSpecSync({
35
+ * Name: 'My New Agent',
36
+ * Description: 'Does amazing things',
37
+ * IconClass: 'fa-robot',
38
+ * InvocationMode: 'Any'
39
+ * }, contextUser);
40
+ *
41
+ * const agentId = await newAgent.SaveToDatabase();
42
+ * ```
43
+ *
44
+ * ### Modify and save
45
+ * ```typescript
46
+ * const spec = await AgentSpecSync.LoadFromDatabase('agent-uuid', contextUser);
47
+ * spec.spec.Description = 'Updated description';
48
+ * spec.spec.MaxCostPerRun = 10.00;
49
+ * spec.markDirty();
50
+ * await spec.SaveToDatabase();
51
+ * ```
52
+ *
53
+ * @module @memberjunction/ai-agent-manager
54
+ */
55
+ class AgentSpecSync {
56
+ /**
57
+ * Create a new AgentSpecSync instance.
58
+ *
59
+ * Note: This constructor is typically not called directly. Instead, use the static factory methods:
60
+ * - {@link LoadFromDatabase} - Load existing agent from database
61
+ * - {@link LoadByName} - Load agent by name
62
+ * - {@link FromRawSpec} - Create from raw spec data
63
+ *
64
+ * @param spec - Optional initial spec data (for creating new agents or working with existing data)
65
+ * @param contextUser - Optional context user (required for server-side operations)
66
+ */
67
+ constructor(spec, contextUser) {
68
+ /**
69
+ * Tracks whether this spec has been loaded from the database
70
+ * @private
71
+ */
72
+ this._isLoaded = false;
73
+ /**
74
+ * Tracks whether this spec has unsaved changes
75
+ * @private
76
+ */
77
+ this._isDirty = false;
78
+ if (spec) {
79
+ this.spec = this.initializeSpec(spec);
80
+ this._isDirty = true;
81
+ }
82
+ else {
83
+ // Create minimal empty spec
84
+ this.spec = {
85
+ ID: '', // Will be set on save if empty
86
+ Name: '',
87
+ StartingPayloadValidationMode: () => 'Fail',
88
+ Actions: [],
89
+ SubAgents: []
90
+ };
91
+ }
92
+ this._contextUser = contextUser;
93
+ }
94
+ // ===== STATIC FACTORY METHODS =====
95
+ /**
96
+ * Load an agent and its complete hierarchy from the database by ID.
97
+ *
98
+ * This method efficiently loads the agent along with all its actions and sub-agents
99
+ * using batched queries to minimize database round trips. When `includeSubAgents` is true,
100
+ * it recursively loads the entire agent hierarchy.
101
+ *
102
+ * @param agentId - The unique ID of the agent to load
103
+ * @param contextUser - Optional context user (required for server-side operations)
104
+ * @param includeSubAgents - Whether to recursively load all sub-agents (default: true)
105
+ * @returns Promise resolving to AgentSpecSync instance with loaded data
106
+ * @throws {Error} If agent with specified ID is not found
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * // Load agent with all sub-agents
111
+ * const spec = await AgentSpecSync.LoadFromDatabase(
112
+ * 'agent-uuid-here',
113
+ * contextUser,
114
+ * true
115
+ * );
116
+ * ```
117
+ */
118
+ static async LoadFromDatabase(agentId, contextUser, includeSubAgents = true) {
119
+ const instance = new AgentSpecSync(undefined, contextUser);
120
+ await instance.loadFromEntities(agentId, includeSubAgents);
121
+ return instance;
122
+ }
123
+ /**
124
+ * Load an agent by name (must be unique).
125
+ *
126
+ * Searches for an agent with the specified name and loads it. If multiple agents
127
+ * have the same name, an error is thrown. Agent names should be unique within
128
+ * the system for this method to work reliably.
129
+ *
130
+ * @param agentName - The name of the agent to load
131
+ * @param contextUser - Optional context user (required for server-side operations)
132
+ * @param includeSubAgents - Whether to recursively load sub-agents (default: true)
133
+ * @returns Promise resolving to AgentSpecSync instance with loaded data
134
+ * @throws {Error} If agent is not found or multiple agents have the same name
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const spec = await AgentSpecSync.LoadByName('My Agent', contextUser);
139
+ * ```
140
+ */
141
+ static async LoadByName(agentName, contextUser, includeSubAgents = true) {
142
+ // Find agent by name
143
+ const rv = new core_1.RunView();
144
+ const result = await rv.RunView({
145
+ EntityName: 'AI Agents',
146
+ ExtraFilter: `Name='${agentName.replace(/'/g, "''")}'`,
147
+ ResultType: 'entity_object'
148
+ }, contextUser);
149
+ if (!result.Success) {
150
+ throw new Error(`Failed to find agent by name: ${result.ErrorMessage}`);
151
+ }
152
+ if (!result.Results || result.Results.length === 0) {
153
+ throw new Error(`Agent with name '${agentName}' not found`);
154
+ }
155
+ if (result.Results.length > 1) {
156
+ throw new Error(`Multiple agents found with name '${agentName}'. Use LoadFromDatabase with specific ID instead.`);
157
+ }
158
+ const agent = result.Results[0];
159
+ return AgentSpecSync.LoadFromDatabase(agent.ID, contextUser, includeSubAgents);
160
+ }
161
+ /**
162
+ * Create a new agent spec from a raw specification.
163
+ *
164
+ * This creates an in-memory AgentSpecSync instance from raw data. The agent is not
165
+ * saved to the database until {@link SaveToDatabase} is called.
166
+ *
167
+ * @param rawSpec - The raw spec data conforming to {@link AgentSpec} interface
168
+ * @param contextUser - Optional context user (required when saving server-side)
169
+ * @returns New AgentSpecSync instance (not yet saved to database)
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * const rawSpec: AgentSpec = {
174
+ * ID: '',
175
+ * Name: 'New Agent',
176
+ * Description: 'Agent description',
177
+ * InvocationMode: 'Any',
178
+ * Actions: [],
179
+ * SubAgents: []
180
+ * };
181
+ * const spec = AgentSpecSync.FromRawSpec(rawSpec, contextUser);
182
+ * await spec.SaveToDatabase();
183
+ * ```
184
+ */
185
+ static FromRawSpec(rawSpec, contextUser) {
186
+ return new AgentSpecSync(rawSpec, contextUser);
187
+ }
188
+ // ===== LOADING METHODS =====
189
+ /**
190
+ * Load the complete agent specification from database entities.
191
+ *
192
+ * This method orchestrates loading from AIAgent, AIAgentAction, and AIAgentRelationship tables
193
+ * using batched queries for optimal performance. It handles both child agents (ParentID-based)
194
+ * and related agents (relationship-based).
195
+ *
196
+ * @private
197
+ * @param agentId - The agent ID to load
198
+ * @param includeSubAgents - Whether to recursively load sub-agents
199
+ * @throws {Error} If agent is not found
200
+ */
201
+ async loadFromEntities(agentId, includeSubAgents) {
202
+ const md = new core_1.Metadata();
203
+ const rv = new core_1.RunView();
204
+ // Step 1: Load the main agent entity
205
+ const agentEntity = await md.GetEntityObject('AI Agents', this._contextUser);
206
+ const loaded = await agentEntity.Load(agentId);
207
+ if (!loaded) {
208
+ throw new Error(`Agent with ID ${agentId} not found`);
209
+ }
210
+ // Step 2: Batch load related entities using RunViews for optimal performance
211
+ const [actionsResult, childAgentsResult, relatedAgentsResult] = await rv.RunViews([
212
+ {
213
+ EntityName: 'AI Agent Actions',
214
+ ExtraFilter: `AgentID='${agentId}'`,
215
+ OrderBy: 'Status, ActionID',
216
+ ResultType: 'entity_object'
217
+ },
218
+ {
219
+ EntityName: 'AI Agents',
220
+ ExtraFilter: `ParentID='${agentId}'`,
221
+ OrderBy: 'ExecutionOrder, Name',
222
+ ResultType: 'entity_object'
223
+ },
224
+ {
225
+ EntityName: 'MJ: AI Agent Relationships',
226
+ ExtraFilter: `AgentID='${agentId}' AND Status='Active'`,
227
+ OrderBy: '__mj_CreatedAt',
228
+ ResultType: 'entity_object'
229
+ }
230
+ ], this._contextUser);
231
+ // Check for errors
232
+ if (!actionsResult.Success) {
233
+ throw new Error(`Failed to load agent actions: ${actionsResult.ErrorMessage}`);
234
+ }
235
+ if (!childAgentsResult.Success) {
236
+ throw new Error(`Failed to load child agents: ${childAgentsResult.ErrorMessage}`);
237
+ }
238
+ if (!relatedAgentsResult.Success) {
239
+ throw new Error(`Failed to load related agents: ${relatedAgentsResult.ErrorMessage}`);
240
+ }
241
+ // Step 3: Map entities to raw spec format
242
+ this.spec = this.mapEntitiesToRawSpec(agentEntity, actionsResult.Results || [], childAgentsResult.Results || [], relatedAgentsResult.Results || []);
243
+ // Step 4: Recursively load sub-agents if requested
244
+ if (includeSubAgents && this.spec.SubAgents && this.spec.SubAgents.length > 0) {
245
+ // Note: We don't recursively populate the full spec here since SubAgentSpec
246
+ // only contains the ID and relationship metadata. To get full details,
247
+ // users can call LoadFromDatabase on the SubAgentID separately if needed.
248
+ }
249
+ this._isLoaded = true;
250
+ this._isDirty = false;
251
+ }
252
+ /**
253
+ * Map database entities to AgentSpec format.
254
+ *
255
+ * Transforms the normalized database entities into a single denormalized specification
256
+ * object that's easy to work with in code. Handles JSON parsing for all structured fields.
257
+ *
258
+ * @private
259
+ * @param agent - The main agent entity
260
+ * @param actions - Array of agent action entities
261
+ * @param childAgents - Array of child agent entities (ParentID-based)
262
+ * @param relatedAgents - Array of related agent relationship entities
263
+ * @returns Fully populated AgentSpec object
264
+ */
265
+ mapEntitiesToRawSpec(agent, actions, childAgents, relatedAgents) {
266
+ // Map all agent fields to spec
267
+ const spec = {
268
+ ID: agent.ID,
269
+ Name: agent.Name || '',
270
+ Description: agent.Description || undefined,
271
+ IconClass: agent.IconClass || undefined,
272
+ LogoURL: agent.LogoURL || undefined,
273
+ ParentID: agent.ParentID || undefined,
274
+ DriverClass: agent.DriverClass || undefined,
275
+ ModelSelectionMode: agent.ModelSelectionMode,
276
+ // Parse JSON array fields
277
+ PayloadDownstreamPaths: this.parseJsonField(agent.PayloadDownstreamPaths),
278
+ PayloadUpstreamPaths: this.parseJsonField(agent.PayloadUpstreamPaths),
279
+ PayloadSelfReadPaths: this.parseJsonField(agent.PayloadSelfReadPaths),
280
+ PayloadSelfWritePaths: this.parseJsonField(agent.PayloadSelfWritePaths),
281
+ PayloadScope: agent.PayloadScope || undefined,
282
+ // Validation fields
283
+ FinalPayloadValidation: agent.FinalPayloadValidation || null,
284
+ FinalPayloadValidationMode: agent.FinalPayloadValidationMode,
285
+ FinalPayloadValidationMaxRetries: agent.FinalPayloadValidationMaxRetries || undefined,
286
+ StartingPayloadValidation: agent.StartingPayloadValidation || null,
287
+ StartingPayloadValidationMode: agent.StartingPayloadValidationMode,
288
+ // Resource limits
289
+ MaxCostPerRun: agent.MaxCostPerRun || null,
290
+ MaxTokensPerRun: agent.MaxTokensPerRun || null,
291
+ MaxIterationsPerRun: agent.MaxIterationsPerRun || null,
292
+ MaxTimePerRun: agent.MaxTimePerRun || undefined,
293
+ // Execution frequency
294
+ MinExecutionsPerRun: agent.MinExecutionsPerRun || undefined,
295
+ MaxExecutionsPerRun: agent.MaxExecutionsPerRun || undefined,
296
+ // Other config
297
+ DefaultPromptEffortLevel: agent.DefaultPromptEffortLevel || undefined,
298
+ ChatHandlingOption: agent.ChatHandlingOption || undefined,
299
+ DefaultArtifactTypeID: agent.DefaultArtifactTypeID || undefined,
300
+ OwnerUserID: agent.OwnerUserID || undefined,
301
+ InvocationMode: agent.InvocationMode,
302
+ // Map actions
303
+ Actions: actions.map(action => this.mapActionEntityToSpec(action)),
304
+ // Map sub-agents (both child and related)
305
+ SubAgents: [
306
+ ...childAgents.map(child => this.mapChildAgentToSpec(child)),
307
+ ...relatedAgents.map(rel => this.mapRelatedAgentToSpec(rel))
308
+ ]
309
+ };
310
+ return spec;
311
+ }
312
+ /**
313
+ * Map AIAgentActionEntity to AgentActionSpec format.
314
+ *
315
+ * @private
316
+ * @param action - The agent action entity from the database
317
+ * @returns Mapped action spec
318
+ */
319
+ mapActionEntityToSpec(action) {
320
+ return {
321
+ AgentActionID: action.ID,
322
+ ActionID: action.ActionID || '',
323
+ Status: action.Status,
324
+ MaxExecutionsPerRun: action.MaxExecutionsPerRun || undefined,
325
+ ResultExpirationTurns: action.ResultExpirationTurns || undefined,
326
+ ResultExpirationMode: action.ResultExpirationMode || undefined,
327
+ CompactMode: action.CompactMode || undefined,
328
+ CompactLength: action.CompactLength || undefined,
329
+ CompactPromptID: action.CompactPromptID || null
330
+ };
331
+ }
332
+ /**
333
+ * Map child agent (ParentID-based) to SubAgentSpec format.
334
+ *
335
+ * @private
336
+ * @param childAgent - The child agent entity
337
+ * @returns Mapped sub-agent spec
338
+ */
339
+ mapChildAgentToSpec(childAgent) {
340
+ return {
341
+ Type: 'child',
342
+ SubAgentID: childAgent.ID
343
+ };
344
+ }
345
+ /**
346
+ * Map related agent (relationship-based) to SubAgentSpec format.
347
+ *
348
+ * @private
349
+ * @param relationship - The agent relationship entity
350
+ * @returns Mapped sub-agent spec
351
+ */
352
+ mapRelatedAgentToSpec(relationship) {
353
+ return {
354
+ Type: 'related',
355
+ SubAgentID: relationship.SubAgentID,
356
+ AgentRelationshipID: relationship.ID,
357
+ SubAgentInputMapping: this.parseJsonField(relationship.SubAgentInputMapping),
358
+ SubAgentOutputMapping: this.parseJsonField(relationship.SubAgentOutputMapping),
359
+ SubAgentContextPaths: this.parseJsonField(relationship.SubAgentContextPaths)
360
+ };
361
+ }
362
+ // ===== SAVING METHODS =====
363
+ /**
364
+ * Save the current spec to the database.
365
+ *
366
+ * This will create new records or update existing ones based on whether IDs exist.
367
+ * The save operation is performed atomically - if any part fails, no changes are committed.
368
+ *
369
+ * Validation is performed before saving. All entity-level validations defined in the
370
+ * database schema are executed, and any failures will prevent the save.
371
+ *
372
+ * @param validate - Whether to validate before saving (default: true)
373
+ * @returns Promise resolving to the saved agent ID
374
+ * @throws {Error} If validation fails or save operation fails
375
+ *
376
+ * @example
377
+ * ```typescript
378
+ * const spec = new AgentSpecSync({
379
+ * Name: 'New Agent',
380
+ * InvocationMode: 'Any'
381
+ * }, contextUser);
382
+ *
383
+ * const agentId = await spec.SaveToDatabase();
384
+ * console.log('Saved with ID:', agentId);
385
+ * ```
386
+ */
387
+ async SaveToDatabase(validate = true) {
388
+ if (!this._isDirty && this._isLoaded) {
389
+ // No changes to save
390
+ return this.spec.ID;
391
+ }
392
+ // Step 1: Save main agent entity
393
+ const agentId = await this.saveAgentEntity(validate);
394
+ // Step 2: Save actions
395
+ await this.saveActions(agentId);
396
+ // Step 3: Save sub-agents (both child and related)
397
+ await this.saveSubAgents(agentId);
398
+ this._isDirty = false;
399
+ this._isLoaded = true;
400
+ return agentId;
401
+ }
402
+ /**
403
+ * Save the main AIAgent entity.
404
+ *
405
+ * @private
406
+ * @param validate - Whether to perform validation before saving
407
+ * @returns Promise resolving to the saved agent ID
408
+ * @throws {Error} If validation fails or save fails
409
+ */
410
+ async saveAgentEntity(validate) {
411
+ const md = new core_1.Metadata();
412
+ const agentEntity = await md.GetEntityObject('AI Agents', this._contextUser);
413
+ // If ID exists, load existing record
414
+ if (this.spec.ID) {
415
+ const loaded = await agentEntity.Load(this.spec.ID);
416
+ if (!loaded) {
417
+ throw new Error(`Cannot update non-existent agent with ID ${this.spec.ID}`);
418
+ }
419
+ }
420
+ // Map spec to entity fields
421
+ agentEntity.Name = this.spec.Name;
422
+ agentEntity.Description = this.spec.Description || null;
423
+ agentEntity.IconClass = this.spec.IconClass || null;
424
+ agentEntity.LogoURL = this.spec.LogoURL || null;
425
+ agentEntity.ParentID = this.spec.ParentID || null;
426
+ agentEntity.DriverClass = this.spec.DriverClass || null;
427
+ agentEntity.ModelSelectionMode = this.spec.ModelSelectionMode || 'Agent Type';
428
+ // Serialize JSON fields
429
+ agentEntity.PayloadDownstreamPaths = JSON.stringify(this.spec.PayloadDownstreamPaths || ['*']);
430
+ agentEntity.PayloadUpstreamPaths = JSON.stringify(this.spec.PayloadUpstreamPaths || ['*']);
431
+ agentEntity.PayloadSelfReadPaths = this.spec.PayloadSelfReadPaths
432
+ ? JSON.stringify(this.spec.PayloadSelfReadPaths)
433
+ : null;
434
+ agentEntity.PayloadSelfWritePaths = this.spec.PayloadSelfWritePaths
435
+ ? JSON.stringify(this.spec.PayloadSelfWritePaths)
436
+ : null;
437
+ agentEntity.PayloadScope = this.spec.PayloadScope || null;
438
+ // Validation fields
439
+ agentEntity.FinalPayloadValidation = this.spec.FinalPayloadValidation || null;
440
+ agentEntity.FinalPayloadValidationMode = this.spec.FinalPayloadValidationMode || 'Retry';
441
+ agentEntity.FinalPayloadValidationMaxRetries = this.spec.FinalPayloadValidationMaxRetries || 3;
442
+ agentEntity.StartingPayloadValidation = this.spec.StartingPayloadValidation || null;
443
+ agentEntity.StartingPayloadValidationMode = this.spec.StartingPayloadValidationMode?.() || 'Fail';
444
+ // Resource limits
445
+ agentEntity.MaxCostPerRun = this.spec.MaxCostPerRun || null;
446
+ agentEntity.MaxTokensPerRun = this.spec.MaxTokensPerRun || null;
447
+ agentEntity.MaxIterationsPerRun = this.spec.MaxIterationsPerRun || null;
448
+ agentEntity.MaxTimePerRun = this.spec.MaxTimePerRun || null;
449
+ // Execution frequency
450
+ agentEntity.MinExecutionsPerRun = this.spec.MinExecutionsPerRun || null;
451
+ agentEntity.MaxExecutionsPerRun = this.spec.MaxExecutionsPerRun || null;
452
+ // Other config
453
+ agentEntity.DefaultPromptEffortLevel = this.spec.DefaultPromptEffortLevel || null;
454
+ agentEntity.ChatHandlingOption = this.spec.ChatHandlingOption || null;
455
+ agentEntity.DefaultArtifactTypeID = this.spec.DefaultArtifactTypeID || null;
456
+ if (this.spec.OwnerUserID) {
457
+ agentEntity.OwnerUserID = this.spec.OwnerUserID;
458
+ }
459
+ agentEntity.InvocationMode = this.spec.InvocationMode || 'Any';
460
+ // Validate if requested
461
+ if (validate) {
462
+ const validation = agentEntity.Validate();
463
+ if (!validation.Success) {
464
+ const errors = validation.Errors.map(e => e.Message).join(', ');
465
+ throw new Error(`Agent validation failed: ${errors}`);
466
+ }
467
+ }
468
+ // Save
469
+ const saved = await agentEntity.Save();
470
+ if (!saved) {
471
+ throw new Error('Failed to save agent entity');
472
+ }
473
+ // Update spec with saved ID
474
+ this.spec.ID = agentEntity.ID;
475
+ return agentEntity.ID;
476
+ }
477
+ /**
478
+ * Save all actions for this agent.
479
+ *
480
+ * Creates or updates agent action records. Existing actions are updated, new actions
481
+ * are created. This method does not delete actions that are no longer in the spec -
482
+ * use a separate delete method for that.
483
+ *
484
+ * @private
485
+ * @param agentId - The parent agent ID
486
+ * @throws {Error} If any action save fails
487
+ */
488
+ async saveActions(agentId) {
489
+ if (!this.spec.Actions || this.spec.Actions.length === 0) {
490
+ return;
491
+ }
492
+ const md = new core_1.Metadata();
493
+ for (const actionSpec of this.spec.Actions) {
494
+ const actionEntity = await md.GetEntityObject('AI Agent Actions', this._contextUser);
495
+ // Load existing if ID provided
496
+ if (actionSpec.AgentActionID) {
497
+ await actionEntity.Load(actionSpec.AgentActionID);
498
+ }
499
+ // Map fields
500
+ actionEntity.AgentID = agentId;
501
+ actionEntity.ActionID = actionSpec.ActionID;
502
+ actionEntity.Status = actionSpec.Status;
503
+ actionEntity.MaxExecutionsPerRun = actionSpec.MaxExecutionsPerRun || null;
504
+ actionEntity.ResultExpirationTurns = actionSpec.ResultExpirationTurns || null;
505
+ actionEntity.ResultExpirationMode = actionSpec.ResultExpirationMode || 'None';
506
+ actionEntity.CompactMode = actionSpec.CompactMode || null;
507
+ actionEntity.CompactLength = actionSpec.CompactLength || null;
508
+ actionEntity.CompactPromptID = actionSpec.CompactPromptID || null;
509
+ const saved = await actionEntity.Save();
510
+ if (!saved) {
511
+ throw new Error(`Failed to save action ${actionSpec.ActionID}`);
512
+ }
513
+ // Update spec with saved ID
514
+ actionSpec.AgentActionID = actionEntity.ID;
515
+ }
516
+ }
517
+ /**
518
+ * Save all sub-agents (both child and related types).
519
+ *
520
+ * Handles both ParentID-based child agents and relationship-based related agents.
521
+ * For child agents, updates the ParentID on the sub-agent entity. For related agents,
522
+ * creates or updates the relationship record.
523
+ *
524
+ * @private
525
+ * @param agentId - The parent agent ID
526
+ * @throws {Error} If any sub-agent save fails
527
+ */
528
+ async saveSubAgents(agentId) {
529
+ if (!this.spec.SubAgents || this.spec.SubAgents.length === 0) {
530
+ return;
531
+ }
532
+ for (const SubAgentSpec of this.spec.SubAgents) {
533
+ if (SubAgentSpec.Type === 'child') {
534
+ await this.saveChildSubAgent(agentId, SubAgentSpec);
535
+ }
536
+ else {
537
+ await this.saveRelatedSubAgent(agentId, SubAgentSpec);
538
+ }
539
+ }
540
+ }
541
+ /**
542
+ * Save a child sub-agent (ParentID-based relationship).
543
+ *
544
+ * For child agents, the relationship is established by setting the ParentID field
545
+ * on the child agent entity. If the SubAgentID is empty, this indicates a new
546
+ * child agent that needs to be created.
547
+ *
548
+ * @private
549
+ * @param parentId - The parent agent ID
550
+ * @param SubAgentSpec - The sub-agent specification
551
+ * @throws {Error} If child agent doesn't exist or save fails
552
+ */
553
+ async saveChildSubAgent(parentId, SubAgentSpec) {
554
+ if (!SubAgentSpec.SubAgentID) {
555
+ throw new Error('Child sub-agent must have a SubAgentID');
556
+ }
557
+ // For child agents, we just need to ensure the ParentID is set correctly
558
+ // The sub-agent itself should already exist or be created separately
559
+ const md = new core_1.Metadata();
560
+ const childEntity = await md.GetEntityObject('AI Agents', this._contextUser);
561
+ const loaded = await childEntity.Load(SubAgentSpec.SubAgentID);
562
+ if (!loaded) {
563
+ throw new Error(`Child agent ${SubAgentSpec.SubAgentID} not found`);
564
+ }
565
+ // Update parent ID if needed
566
+ if (childEntity.ParentID !== parentId) {
567
+ childEntity.ParentID = parentId;
568
+ const saved = await childEntity.Save();
569
+ if (!saved) {
570
+ throw new Error(`Failed to update ParentID for child agent ${SubAgentSpec.SubAgentID}`);
571
+ }
572
+ }
573
+ }
574
+ /**
575
+ * Save a related sub-agent (relationship-based).
576
+ *
577
+ * Creates or updates an AIAgentRelationship record that links the parent and
578
+ * sub-agent. This includes the input/output mapping and context path configurations.
579
+ *
580
+ * @private
581
+ * @param agentId - The parent agent ID
582
+ * @param SubAgentSpec - The sub-agent specification
583
+ * @throws {Error} If relationship save fails
584
+ */
585
+ async saveRelatedSubAgent(agentId, SubAgentSpec) {
586
+ const md = new core_1.Metadata();
587
+ const relationshipEntity = await md.GetEntityObject('MJ: AI Agent Relationships', this._contextUser);
588
+ // Load existing if ID provided
589
+ if (SubAgentSpec.AgentRelationshipID) {
590
+ await relationshipEntity.Load(SubAgentSpec.AgentRelationshipID);
591
+ }
592
+ // Map fields
593
+ relationshipEntity.AgentID = agentId;
594
+ relationshipEntity.SubAgentID = SubAgentSpec.SubAgentID;
595
+ relationshipEntity.Status = 'Active';
596
+ // Serialize mapping fields
597
+ if (SubAgentSpec.SubAgentInputMapping) {
598
+ relationshipEntity.SubAgentInputMapping = JSON.stringify(SubAgentSpec.SubAgentInputMapping);
599
+ }
600
+ else {
601
+ relationshipEntity.SubAgentInputMapping = null;
602
+ }
603
+ if (SubAgentSpec.SubAgentOutputMapping) {
604
+ relationshipEntity.SubAgentOutputMapping = JSON.stringify(SubAgentSpec.SubAgentOutputMapping);
605
+ }
606
+ else {
607
+ relationshipEntity.SubAgentOutputMapping = null;
608
+ }
609
+ if (SubAgentSpec.SubAgentContextPaths) {
610
+ relationshipEntity.SubAgentContextPaths = JSON.stringify(SubAgentSpec.SubAgentContextPaths);
611
+ }
612
+ else {
613
+ relationshipEntity.SubAgentContextPaths = null;
614
+ }
615
+ const saved = await relationshipEntity.Save();
616
+ if (!saved) {
617
+ throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgentID}`);
618
+ }
619
+ // Update spec with saved ID
620
+ SubAgentSpec.AgentRelationshipID = relationshipEntity.ID;
621
+ }
622
+ // ===== UTILITY METHODS =====
623
+ /**
624
+ * Parse a JSON string field, returning undefined if null/empty.
625
+ *
626
+ * Safely parses JSON fields from the database, handling null/undefined values
627
+ * and logging errors if parsing fails.
628
+ *
629
+ * @private
630
+ * @param jsonString - The JSON string to parse
631
+ * @returns Parsed object or undefined if null/empty/invalid
632
+ */
633
+ parseJsonField(jsonString) {
634
+ if (!jsonString)
635
+ return undefined;
636
+ try {
637
+ return JSON.parse(jsonString);
638
+ }
639
+ catch (error) {
640
+ (0, core_1.LogError)(`Failed to parse JSON field: ${error}`);
641
+ return undefined;
642
+ }
643
+ }
644
+ /**
645
+ * Initialize a spec with defaults for any missing required fields.
646
+ *
647
+ * Takes a partial spec and fills in defaults for any missing fields to ensure
648
+ * a valid AgentSpec structure.
649
+ *
650
+ * @private
651
+ * @param partial - Partial agent specification
652
+ * @returns Complete AgentSpec with defaults
653
+ */
654
+ initializeSpec(partial) {
655
+ return {
656
+ ID: partial.ID || '',
657
+ Name: partial.Name || '',
658
+ Description: partial.Description,
659
+ IconClass: partial.IconClass,
660
+ LogoURL: partial.LogoURL,
661
+ ParentID: partial.ParentID,
662
+ DriverClass: partial.DriverClass,
663
+ ModelSelectionMode: partial.ModelSelectionMode || 'Agent Type',
664
+ PayloadDownstreamPaths: partial.PayloadDownstreamPaths,
665
+ PayloadUpstreamPaths: partial.PayloadUpstreamPaths,
666
+ PayloadSelfReadPaths: partial.PayloadSelfReadPaths,
667
+ PayloadSelfWritePaths: partial.PayloadSelfWritePaths,
668
+ PayloadScope: partial.PayloadScope,
669
+ FinalPayloadValidation: partial.FinalPayloadValidation,
670
+ FinalPayloadValidationMode: partial.FinalPayloadValidationMode || 'Retry',
671
+ FinalPayloadValidationMaxRetries: partial.FinalPayloadValidationMaxRetries,
672
+ MaxCostPerRun: partial.MaxCostPerRun,
673
+ MaxTokensPerRun: partial.MaxTokensPerRun,
674
+ MaxIterationsPerRun: partial.MaxIterationsPerRun,
675
+ MaxTimePerRun: partial.MaxTimePerRun,
676
+ MinExecutionsPerRun: partial.MinExecutionsPerRun,
677
+ MaxExecutionsPerRun: partial.MaxExecutionsPerRun,
678
+ StartingPayloadValidation: partial.StartingPayloadValidation,
679
+ StartingPayloadValidationMode: partial.StartingPayloadValidationMode || (() => 'Fail'),
680
+ DefaultPromptEffortLevel: partial.DefaultPromptEffortLevel,
681
+ ChatHandlingOption: partial.ChatHandlingOption,
682
+ DefaultArtifactTypeID: partial.DefaultArtifactTypeID,
683
+ OwnerUserID: partial.OwnerUserID,
684
+ InvocationMode: partial.InvocationMode || 'Any',
685
+ Actions: partial.Actions || [],
686
+ SubAgents: partial.SubAgents || []
687
+ };
688
+ }
689
+ /**
690
+ * Get a clean serializable version of the spec.
691
+ *
692
+ * Returns a plain JavaScript object suitable for JSON serialization,
693
+ * API responses, or storage. This is useful when you need to send the
694
+ * agent spec over the wire or store it in a file.
695
+ *
696
+ * @returns Clean copy of the agent specification
697
+ *
698
+ * @example
699
+ * ```typescript
700
+ * const spec = await AgentSpecSync.LoadFromDatabase('agent-uuid', contextUser);
701
+ * const json = spec.toJSON();
702
+ * res.json(json); // Send as API response
703
+ * ```
704
+ */
705
+ toJSON() {
706
+ return { ...this.spec };
707
+ }
708
+ /**
709
+ * Check if this spec has unsaved changes.
710
+ *
711
+ * @returns True if there are unsaved changes, false otherwise
712
+ */
713
+ get isDirty() {
714
+ return this._isDirty;
715
+ }
716
+ /**
717
+ * Check if this spec has been loaded from the database.
718
+ *
719
+ * @returns True if loaded from database, false if created in memory
720
+ */
721
+ get isLoaded() {
722
+ return this._isLoaded;
723
+ }
724
+ /**
725
+ * Mark the spec as having changes.
726
+ *
727
+ * Call this method after modifying the spec to indicate that changes need to be saved.
728
+ * The spec is automatically marked dirty when created with the constructor, but if you
729
+ * load a spec and then modify it, you should call this method.
730
+ *
731
+ * @example
732
+ * ```typescript
733
+ * const spec = await AgentSpecSync.LoadFromDatabase('agent-uuid', contextUser);
734
+ * spec.spec.Description = 'New description';
735
+ * spec.markDirty();
736
+ * await spec.SaveToDatabase();
737
+ * ```
738
+ */
739
+ markDirty() {
740
+ this._isDirty = true;
741
+ }
742
+ }
743
+ exports.AgentSpecSync = AgentSpecSync;
744
+ //# sourceMappingURL=agent-spec-sync.js.map