@memberjunction/ai-agent-manager 2.108.0 → 2.110.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.
@@ -75,6 +75,11 @@ class AgentSpecSync {
75
75
  * @private
76
76
  */
77
77
  this._isDirty = false;
78
+ /**
79
+ * Tracks all database mutations performed during save operations
80
+ * @private
81
+ */
82
+ this._mutations = [];
78
83
  if (spec) {
79
84
  this.spec = this.initializeSpec(spec);
80
85
  this._isDirty = true;
@@ -84,13 +89,39 @@ class AgentSpecSync {
84
89
  this.spec = {
85
90
  ID: '', // Will be set on save if empty
86
91
  Name: '',
87
- StartingPayloadValidationMode: () => 'Fail',
92
+ StartingPayloadValidationMode: 'Fail',
88
93
  Actions: [],
89
94
  SubAgents: []
90
95
  };
91
96
  }
92
97
  this._contextUser = contextUser;
93
98
  }
99
+ // ===== MUTATION TRACKING METHODS =====
100
+ /**
101
+ * Track a database mutation performed during save operation
102
+ * @private
103
+ */
104
+ trackMutation(entity, operation, id, description) {
105
+ this._mutations.push({
106
+ Entity: entity,
107
+ Operation: operation,
108
+ ID: id,
109
+ Description: description
110
+ });
111
+ }
112
+ /**
113
+ * Get all mutations tracked during the last save operation
114
+ * @returns Array of mutations
115
+ */
116
+ getMutations() {
117
+ return [...this._mutations];
118
+ }
119
+ /**
120
+ * Clear all tracked mutations
121
+ */
122
+ clearMutations() {
123
+ this._mutations = [];
124
+ }
94
125
  // ===== STATIC FACTORY METHODS =====
95
126
  /**
96
127
  * Load an agent and its complete hierarchy from the database by ID.
@@ -208,7 +239,8 @@ class AgentSpecSync {
208
239
  throw new Error(`Agent with ID ${agentId} not found`);
209
240
  }
210
241
  // Step 2: Batch load related entities using RunViews for optimal performance
211
- const [actionsResult, childAgentsResult, relatedAgentsResult] = await rv.RunViews([
242
+ // Note: Load paths separately after steps to avoid hardcoded view name in subquery
243
+ const [actionsResult, childAgentsResult, relatedAgentsResult, promptsResult, stepsResult] = await rv.RunViews([
212
244
  {
213
245
  EntityName: 'AI Agent Actions',
214
246
  ExtraFilter: `AgentID='${agentId}'`,
@@ -226,6 +258,18 @@ class AgentSpecSync {
226
258
  ExtraFilter: `AgentID='${agentId}' AND Status='Active'`,
227
259
  OrderBy: '__mj_CreatedAt',
228
260
  ResultType: 'entity_object'
261
+ },
262
+ {
263
+ EntityName: 'MJ: AI Agent Prompts',
264
+ ExtraFilter: `AgentID='${agentId}'`,
265
+ OrderBy: 'ExecutionOrder',
266
+ ResultType: 'entity_object'
267
+ },
268
+ {
269
+ EntityName: 'MJ: AI Agent Steps',
270
+ ExtraFilter: `AgentID='${agentId}'`,
271
+ OrderBy: 'Name',
272
+ ResultType: 'entity_object'
229
273
  }
230
274
  ], this._contextUser);
231
275
  // Check for errors
@@ -238,13 +282,84 @@ class AgentSpecSync {
238
282
  if (!relatedAgentsResult.Success) {
239
283
  throw new Error(`Failed to load related agents: ${relatedAgentsResult.ErrorMessage}`);
240
284
  }
285
+ if (!promptsResult.Success) {
286
+ throw new Error(`Failed to load agent prompts: ${promptsResult.ErrorMessage}`);
287
+ }
288
+ if (!stepsResult.Success) {
289
+ throw new Error(`Failed to load agent steps: ${stepsResult.ErrorMessage}`);
290
+ }
291
+ // Step 2b: Load paths separately using step IDs to avoid hardcoded view name
292
+ let pathsResult;
293
+ const steps = stepsResult.Results || [];
294
+ if (steps.length > 0) {
295
+ const stepIds = steps.map((s) => `'${s.ID}'`).join(',');
296
+ pathsResult = await rv.RunView({
297
+ EntityName: 'MJ: AI Agent Step Paths',
298
+ ExtraFilter: `OriginStepID IN (${stepIds})`,
299
+ OrderBy: 'Priority DESC',
300
+ ResultType: 'entity_object'
301
+ }, this._contextUser);
302
+ if (!pathsResult.Success) {
303
+ throw new Error(`Failed to load step paths: ${pathsResult.ErrorMessage}`);
304
+ }
305
+ }
306
+ else {
307
+ // No steps, so no paths
308
+ pathsResult = {
309
+ Success: true,
310
+ Results: [],
311
+ RowCount: 0
312
+ };
313
+ }
314
+ // Step 2c: Load full AI Prompt records to get PromptText, PromptRole, PromptPosition
315
+ let fullPromptsResult;
316
+ const agentPrompts = promptsResult.Results || [];
317
+ if (agentPrompts.length > 0) {
318
+ const promptIds = agentPrompts.map((p) => `'${p.PromptID}'`).join(',');
319
+ fullPromptsResult = await rv.RunView({
320
+ EntityName: 'AI Prompts',
321
+ ExtraFilter: `ID IN (${promptIds})`,
322
+ OrderBy: 'Name',
323
+ ResultType: 'entity_object'
324
+ }, this._contextUser);
325
+ if (!fullPromptsResult.Success) {
326
+ throw new Error(`Failed to load AI prompts: ${fullPromptsResult.ErrorMessage}`);
327
+ }
328
+ }
329
+ else {
330
+ // No prompts
331
+ fullPromptsResult = {
332
+ Success: true,
333
+ Results: [],
334
+ RowCount: 0
335
+ };
336
+ }
241
337
  // Step 3: Map entities to raw spec format
242
- this.spec = this.mapEntitiesToRawSpec(agentEntity, actionsResult.Results || [], childAgentsResult.Results || [], relatedAgentsResult.Results || []);
338
+ this.spec = this.mapEntitiesToRawSpec(agentEntity, actionsResult.Results || [], childAgentsResult.Results || [], relatedAgentsResult.Results || [], promptsResult.Results || [], fullPromptsResult.Results || [], stepsResult.Results || [], pathsResult.Results || []);
243
339
  // Step 4: Recursively load sub-agents if requested
244
340
  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.
341
+ // Recursively load complete specs for all sub-agents
342
+ for (const subAgentSpec of this.spec.SubAgents) {
343
+ if (subAgentSpec.SubAgent && subAgentSpec.SubAgent.ID) {
344
+ try {
345
+ // Load the complete sub-agent spec recursively
346
+ const subAgentSync = await AgentSpecSync.LoadFromDatabase(subAgentSpec.SubAgent.ID, this._contextUser, true // Recursively load nested sub-agents too
347
+ );
348
+ // Replace the minimal SubAgent with the complete spec
349
+ const fullSubAgentSpec = subAgentSync.toJSON();
350
+ // Preserve the relationship metadata while adding full spec details
351
+ subAgentSpec.SubAgent = {
352
+ ...fullSubAgentSpec,
353
+ // Ensure we preserve any relationship-specific overrides
354
+ StartingPayloadValidationMode: subAgentSpec.SubAgent.StartingPayloadValidationMode || fullSubAgentSpec.StartingPayloadValidationMode
355
+ };
356
+ }
357
+ catch (error) {
358
+ (0, core_1.LogError)(`Failed to load sub-agent ${subAgentSpec.SubAgent.ID}: ${error instanceof Error ? error.message : String(error)}`);
359
+ // Continue with partial data rather than failing completely
360
+ }
361
+ }
362
+ }
248
363
  }
249
364
  this._isLoaded = true;
250
365
  this._isDirty = false;
@@ -260,14 +375,20 @@ class AgentSpecSync {
260
375
  * @param actions - Array of agent action entities
261
376
  * @param childAgents - Array of child agent entities (ParentID-based)
262
377
  * @param relatedAgents - Array of related agent relationship entities
378
+ * @param agentPrompts - Array of agent prompt junction entities
379
+ * @param fullPrompts - Array of full AI Prompt entities with PromptText, PromptRole, PromptPosition
380
+ * @param steps - Array of agent step entities (for Flow agents)
381
+ * @param paths - Array of step path entities (for Flow agents)
263
382
  * @returns Fully populated AgentSpec object
264
383
  */
265
- mapEntitiesToRawSpec(agent, actions, childAgents, relatedAgents) {
384
+ mapEntitiesToRawSpec(agent, actions, childAgents, relatedAgents, agentPrompts, fullPrompts, steps, paths) {
266
385
  // Map all agent fields to spec
267
386
  const spec = {
268
387
  ID: agent.ID,
269
388
  Name: agent.Name || '',
270
389
  Description: agent.Description || undefined,
390
+ TypeID: agent.TypeID || undefined,
391
+ Status: agent.Status || undefined,
271
392
  IconClass: agent.IconClass || undefined,
272
393
  LogoURL: agent.LogoURL || undefined,
273
394
  ParentID: agent.ParentID || undefined,
@@ -299,13 +420,22 @@ class AgentSpecSync {
299
420
  DefaultArtifactTypeID: agent.DefaultArtifactTypeID || undefined,
300
421
  OwnerUserID: agent.OwnerUserID || undefined,
301
422
  InvocationMode: agent.InvocationMode,
423
+ // Requirements and design documentation
424
+ FunctionalRequirements: agent.FunctionalRequirements || null,
425
+ TechnicalDesign: agent.TechnicalDesign || null,
302
426
  // Map actions
303
427
  Actions: actions.map(action => this.mapActionEntityToSpec(action)),
304
428
  // Map sub-agents (both child and related)
305
429
  SubAgents: [
306
430
  ...childAgents.map(child => this.mapChildAgentToSpec(child)),
307
431
  ...relatedAgents.map(rel => this.mapRelatedAgentToSpec(rel))
308
- ]
432
+ ],
433
+ // Map prompts (agent-level prompts for Loop agents)
434
+ Prompts: agentPrompts.map(agentPrompt => this.mapPromptEntityToSpec(agentPrompt, fullPrompts)),
435
+ // Map steps (for Flow agents)
436
+ Steps: steps.map(step => this.mapStepEntityToSpec(step)),
437
+ // Map paths (for Flow agents)
438
+ Paths: paths.map(path => this.mapPathEntityToSpec(path))
309
439
  };
310
440
  return spec;
311
441
  }
@@ -339,7 +469,11 @@ class AgentSpecSync {
339
469
  mapChildAgentToSpec(childAgent) {
340
470
  return {
341
471
  Type: 'child',
342
- SubAgentID: childAgent.ID
472
+ SubAgent: {
473
+ ID: childAgent.ID,
474
+ Name: childAgent.Name || '',
475
+ StartingPayloadValidationMode: 'Fail'
476
+ }
343
477
  };
344
478
  }
345
479
  /**
@@ -352,13 +486,82 @@ class AgentSpecSync {
352
486
  mapRelatedAgentToSpec(relationship) {
353
487
  return {
354
488
  Type: 'related',
355
- SubAgentID: relationship.SubAgentID,
489
+ SubAgent: {
490
+ ID: relationship.SubAgentID,
491
+ Name: relationship.SubAgent || '',
492
+ StartingPayloadValidationMode: 'Fail'
493
+ },
356
494
  AgentRelationshipID: relationship.ID,
357
495
  SubAgentInputMapping: this.parseJsonField(relationship.SubAgentInputMapping),
358
496
  SubAgentOutputMapping: this.parseJsonField(relationship.SubAgentOutputMapping),
359
497
  SubAgentContextPaths: this.parseJsonField(relationship.SubAgentContextPaths)
360
498
  };
361
499
  }
500
+ /**
501
+ * Map AIAgentPromptEntity junction to AgentPromptSpec format.
502
+ *
503
+ * @private
504
+ * @param agentPrompt - The agent prompt junction entity (has PromptID, ExecutionOrder)
505
+ * @param fullPrompts - Array of full AI Prompt entities
506
+ * @returns Mapped prompt spec
507
+ */
508
+ mapPromptEntityToSpec(agentPrompt, fullPrompts) {
509
+ // Find the full prompt data by matching PromptID
510
+ const fullPrompt = fullPrompts.find((p) => p.ID === agentPrompt.PromptID);
511
+ if (!fullPrompt) {
512
+ // Fallback if prompt not found
513
+ return {
514
+ PromptID: agentPrompt.PromptID || '',
515
+ PromptText: '',
516
+ PromptRole: 'System',
517
+ PromptPosition: 'First'
518
+ };
519
+ }
520
+ return {
521
+ PromptID: fullPrompt.ID || '',
522
+ PromptText: fullPrompt.TemplateText || '',
523
+ PromptRole: fullPrompt.PromptRole || 'System',
524
+ PromptPosition: fullPrompt.PromptPosition || 'First'
525
+ };
526
+ }
527
+ /**
528
+ * Map AIAgentStepEntity to AgentStep format.
529
+ *
530
+ * @private
531
+ * @param step - The agent step entity
532
+ * @returns Mapped step spec
533
+ */
534
+ mapStepEntityToSpec(step) {
535
+ return {
536
+ ID: step.ID,
537
+ Name: step.Name,
538
+ Description: step.Description || undefined,
539
+ StepType: step.StepType,
540
+ StartingStep: step.StartingStep,
541
+ ActionID: step.ActionID || undefined,
542
+ SubAgentID: step.SubAgentID || undefined,
543
+ PromptID: step.PromptID || undefined,
544
+ ActionInputMapping: this.parseJsonField(step.ActionInputMapping),
545
+ ActionOutputMapping: this.parseJsonField(step.ActionOutputMapping)
546
+ };
547
+ }
548
+ /**
549
+ * Map AIAgentStepPathEntity to AgentStepPath format.
550
+ *
551
+ * @private
552
+ * @param path - The agent step path entity
553
+ * @returns Mapped path spec
554
+ */
555
+ mapPathEntityToSpec(path) {
556
+ return {
557
+ ID: path.ID,
558
+ OriginStepID: path.OriginStepID,
559
+ DestinationStepID: path.DestinationStepID,
560
+ Condition: path.Condition || undefined,
561
+ Description: path.Description || undefined,
562
+ Priority: path.Priority
563
+ };
564
+ }
362
565
  // ===== SAVING METHODS =====
363
566
  /**
364
567
  * Save the current spec to the database.
@@ -370,7 +573,7 @@ class AgentSpecSync {
370
573
  * database schema are executed, and any failures will prevent the save.
371
574
  *
372
575
  * @param validate - Whether to validate before saving (default: true)
373
- * @returns Promise resolving to the saved agent ID
576
+ * @returns Promise resolving to result with agent ID, success flag, and all mutations performed
374
577
  * @throws {Error} If validation fails or save operation fails
375
578
  *
376
579
  * @example
@@ -380,24 +583,51 @@ class AgentSpecSync {
380
583
  * InvocationMode: 'Any'
381
584
  * }, contextUser);
382
585
  *
383
- * const agentId = await spec.SaveToDatabase();
384
- * console.log('Saved with ID:', agentId);
586
+ * const result = await spec.SaveToDatabase();
587
+ * console.log('Saved with ID:', result.agentId);
588
+ * console.log('Mutations:', result.mutations.length);
385
589
  * ```
386
590
  */
387
591
  async SaveToDatabase(validate = true) {
592
+ // Clear previous mutations
593
+ this.clearMutations();
388
594
  if (!this._isDirty && this._isLoaded) {
389
595
  // 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;
596
+ return {
597
+ agentId: this.spec.ID,
598
+ success: true,
599
+ mutations: []
600
+ };
601
+ }
602
+ try {
603
+ // Step 1: Save main agent entity
604
+ const agentId = await this.saveAgentEntity(validate);
605
+ // Step 2: Save actions
606
+ await this.saveActions(agentId);
607
+ // Step 3: Save sub-agents (both child and related)
608
+ await this.saveSubAgents(agentId);
609
+ // Step 4: Save prompts
610
+ await this.savePrompts(agentId);
611
+ // Step 5: Save steps (Flow agents only)
612
+ await this.saveSteps(agentId);
613
+ // Step 6: Save step paths (Flow agents only)
614
+ await this.saveStepPaths(agentId);
615
+ this._isDirty = false;
616
+ this._isLoaded = true;
617
+ return {
618
+ agentId,
619
+ success: true,
620
+ mutations: this.getMutations()
621
+ };
622
+ }
623
+ catch (error) {
624
+ // Return failure with whatever mutations were completed before error
625
+ return {
626
+ agentId: this.spec.ID || '',
627
+ success: false,
628
+ mutations: this.getMutations()
629
+ };
630
+ }
401
631
  }
402
632
  /**
403
633
  * Save the main AIAgent entity.
@@ -410,6 +640,8 @@ class AgentSpecSync {
410
640
  async saveAgentEntity(validate) {
411
641
  const md = new core_1.Metadata();
412
642
  const agentEntity = await md.GetEntityObject('AI Agents', this._contextUser);
643
+ // Track if this is an update or create
644
+ const isUpdate = !!this.spec.ID;
413
645
  // If ID exists, load existing record
414
646
  if (this.spec.ID) {
415
647
  const loaded = await agentEntity.Load(this.spec.ID);
@@ -425,6 +657,17 @@ class AgentSpecSync {
425
657
  agentEntity.ParentID = this.spec.ParentID || null;
426
658
  agentEntity.DriverClass = this.spec.DriverClass || null;
427
659
  agentEntity.ModelSelectionMode = this.spec.ModelSelectionMode || 'Agent Type';
660
+ // Handle TypeID - supports lookup references or direct GUID
661
+ if (this.spec.TypeID) {
662
+ agentEntity.TypeID = this.spec.TypeID;
663
+ }
664
+ // Handle Status - defaults to Active if not specified
665
+ if (this.spec.Status) {
666
+ agentEntity.Status = this.spec.Status;
667
+ }
668
+ else {
669
+ agentEntity.Status = 'Active';
670
+ }
428
671
  // Serialize JSON fields
429
672
  agentEntity.PayloadDownstreamPaths = JSON.stringify(this.spec.PayloadDownstreamPaths || ['*']);
430
673
  agentEntity.PayloadUpstreamPaths = JSON.stringify(this.spec.PayloadUpstreamPaths || ['*']);
@@ -440,7 +683,7 @@ class AgentSpecSync {
440
683
  agentEntity.FinalPayloadValidationMode = this.spec.FinalPayloadValidationMode || 'Retry';
441
684
  agentEntity.FinalPayloadValidationMaxRetries = this.spec.FinalPayloadValidationMaxRetries || 3;
442
685
  agentEntity.StartingPayloadValidation = this.spec.StartingPayloadValidation || null;
443
- agentEntity.StartingPayloadValidationMode = this.spec.StartingPayloadValidationMode?.() || 'Fail';
686
+ agentEntity.StartingPayloadValidationMode = this.spec.StartingPayloadValidationMode || 'Fail';
444
687
  // Resource limits
445
688
  agentEntity.MaxCostPerRun = this.spec.MaxCostPerRun || null;
446
689
  agentEntity.MaxTokensPerRun = this.spec.MaxTokensPerRun || null;
@@ -457,6 +700,9 @@ class AgentSpecSync {
457
700
  agentEntity.OwnerUserID = this.spec.OwnerUserID;
458
701
  }
459
702
  agentEntity.InvocationMode = this.spec.InvocationMode || 'Any';
703
+ // Requirements and design documentation
704
+ agentEntity.FunctionalRequirements = this.spec.FunctionalRequirements || null;
705
+ agentEntity.TechnicalDesign = this.spec.TechnicalDesign || null;
460
706
  // Validate if requested
461
707
  if (validate) {
462
708
  const validation = agentEntity.Validate();
@@ -472,6 +718,8 @@ class AgentSpecSync {
472
718
  }
473
719
  // Update spec with saved ID
474
720
  this.spec.ID = agentEntity.ID;
721
+ // Track the mutation
722
+ this.trackMutation('AI Agents', isUpdate ? 'Update' : 'Create', agentEntity.ID, `${isUpdate ? 'Updated' : 'Created'} agent: ${this.spec.Name}`);
475
723
  return agentEntity.ID;
476
724
  }
477
725
  /**
@@ -492,6 +740,8 @@ class AgentSpecSync {
492
740
  const md = new core_1.Metadata();
493
741
  for (const actionSpec of this.spec.Actions) {
494
742
  const actionEntity = await md.GetEntityObject('AI Agent Actions', this._contextUser);
743
+ // Track if this is an update or create
744
+ const isUpdate = !!actionSpec.AgentActionID;
495
745
  // Load existing if ID provided
496
746
  if (actionSpec.AgentActionID) {
497
747
  await actionEntity.Load(actionSpec.AgentActionID);
@@ -512,6 +762,8 @@ class AgentSpecSync {
512
762
  }
513
763
  // Update spec with saved ID
514
764
  actionSpec.AgentActionID = actionEntity.ID;
765
+ // Track the mutation
766
+ this.trackMutation('AI Agent Actions', isUpdate ? 'Update' : 'Create', actionEntity.ID, `${isUpdate ? 'Updated' : 'Created'} action junction for action ${actionSpec.ActionID}`);
515
767
  }
516
768
  }
517
769
  /**
@@ -542,33 +794,49 @@ class AgentSpecSync {
542
794
  * Save a child sub-agent (ParentID-based relationship).
543
795
  *
544
796
  * 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.
797
+ * on the child agent entity. If the SubAgent.ID is empty, this creates a new
798
+ * child agent recursively using AgentSpecSync.
799
+ *
800
+ * This enables creating complete agent hierarchies in one call - sub-agents are
801
+ * created first (depth-first), then parent references them via ParentID.
547
802
  *
548
803
  * @private
549
804
  * @param parentId - The parent agent ID
550
805
  * @param SubAgentSpec - The sub-agent specification
551
- * @throws {Error} If child agent doesn't exist or save fails
806
+ * @throws {Error} If child agent save fails
552
807
  */
553
808
  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`);
809
+ console.log(`🔗 saveChildSubAgent: Processing child "${SubAgentSpec.SubAgent?.Name}", ID="${SubAgentSpec.SubAgent?.ID}"`);
810
+ // If SubAgent.ID is empty or missing, create the sub-agent recursively
811
+ if (!SubAgentSpec.SubAgent?.ID || SubAgentSpec.SubAgent.ID === '') {
812
+ console.log(`🔨 saveChildSubAgent: Creating new child sub-agent "${SubAgentSpec.SubAgent.Name}"...`);
813
+ // Set ParentID in the sub-agent spec before creating it
814
+ const childSpec = {
815
+ ...SubAgentSpec.SubAgent,
816
+ ParentID: parentId
817
+ };
818
+ // Recursively create the sub-agent using AgentSpecSync
819
+ const childSync = new AgentSpecSync(childSpec, this._contextUser);
820
+ childSync.markDirty();
821
+ const childResult = await childSync.SaveToDatabase();
822
+ // Update the SubAgentSpec with the created ID so parent can reference it
823
+ SubAgentSpec.SubAgent.ID = childResult.agentId;
824
+ console.log(`✅ saveChildSubAgent: Created child sub-agent with ID: ${childResult.agentId}`);
564
825
  }
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
- }
826
+ else {
827
+ // SubAgent already exists - update it recursively to capture all field changes
828
+ console.log(`🔗 saveChildSubAgent: Updating existing child sub-agent "${SubAgentSpec.SubAgent.ID}"...`);
829
+ // Set ParentID in the sub-agent spec
830
+ const childSpec = {
831
+ ...SubAgentSpec.SubAgent,
832
+ ParentID: parentId
833
+ };
834
+ // Recursively update the sub-agent using AgentSpecSync
835
+ // This ensures all fields including FunctionalRequirements and TechnicalDesign are updated
836
+ const childSync = new AgentSpecSync(childSpec, this._contextUser);
837
+ childSync.markDirty();
838
+ await childSync.SaveToDatabase();
839
+ console.log(`✅ saveChildSubAgent: Updated existing child sub-agent`);
572
840
  }
573
841
  }
574
842
  /**
@@ -586,12 +854,13 @@ class AgentSpecSync {
586
854
  const md = new core_1.Metadata();
587
855
  const relationshipEntity = await md.GetEntityObject('MJ: AI Agent Relationships', this._contextUser);
588
856
  // Load existing if ID provided
589
- if (SubAgentSpec.AgentRelationshipID) {
857
+ const isUpdate = !!SubAgentSpec.AgentRelationshipID;
858
+ if (isUpdate && SubAgentSpec.AgentRelationshipID) {
590
859
  await relationshipEntity.Load(SubAgentSpec.AgentRelationshipID);
591
860
  }
592
861
  // Map fields
593
862
  relationshipEntity.AgentID = agentId;
594
- relationshipEntity.SubAgentID = SubAgentSpec.SubAgentID;
863
+ relationshipEntity.SubAgentID = SubAgentSpec.SubAgent.ID;
595
864
  relationshipEntity.Status = 'Active';
596
865
  // Serialize mapping fields
597
866
  if (SubAgentSpec.SubAgentInputMapping) {
@@ -614,11 +883,282 @@ class AgentSpecSync {
614
883
  }
615
884
  const saved = await relationshipEntity.Save();
616
885
  if (!saved) {
617
- throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgentID}`);
886
+ throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`);
618
887
  }
888
+ // Track mutation
889
+ this.trackMutation('AI Agent Relationships', isUpdate ? 'Update' : 'Create', relationshipEntity.ID, `${isUpdate ? 'Updated' : 'Created'} agent relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`);
619
890
  // Update spec with saved ID
620
891
  SubAgentSpec.AgentRelationshipID = relationshipEntity.ID;
621
892
  }
893
+ /**
894
+ * Save all prompts for this agent.
895
+ *
896
+ * Creates AIPrompt records (with template) and AIAgentPrompt junction records.
897
+ * Supports simplified prompt format from Architect Agent with just PromptText,
898
+ * PromptRole, and PromptPosition.
899
+ *
900
+ * @private
901
+ * @param agentId - The parent agent ID
902
+ * @throws {Error} If any prompt save fails
903
+ */
904
+ async savePrompts(agentId) {
905
+ console.log(`💬 savePrompts: Called with agentId=${agentId}, Prompts=${this.spec.Prompts ? this.spec.Prompts.length : 'undefined'}`);
906
+ if (!this.spec.Prompts || this.spec.Prompts.length === 0) {
907
+ console.log('💬 savePrompts: No prompts to save, returning early');
908
+ return;
909
+ }
910
+ console.log(`💬 savePrompts: Processing ${this.spec.Prompts.length} prompt(s)...`);
911
+ const md = new core_1.Metadata();
912
+ const rv = new core_1.RunView();
913
+ // Step 1: Create or update AIPrompt records
914
+ for (let i = 0; i < this.spec.Prompts.length; i++) {
915
+ const promptSpec = this.spec.Prompts[i];
916
+ // Check if this is an update (has PromptID) or create (no PromptID)
917
+ let promptEntity = await md.GetEntityObject('AI Prompts', this._contextUser);
918
+ let isUpdate = false;
919
+ if (promptSpec.PromptID && promptSpec.PromptID !== '') {
920
+ // Load existing prompt for update
921
+ const loaded = await promptEntity.Load(promptSpec.PromptID);
922
+ if (loaded) {
923
+ isUpdate = true;
924
+ console.log(`💬 savePrompts: Updating existing prompt ID: ${promptSpec.PromptID}`);
925
+ }
926
+ else {
927
+ console.warn(`💬 savePrompts: PromptID specified but not found, creating new prompt`);
928
+ }
929
+ }
930
+ // Set all required fields (both create and update)
931
+ promptEntity.Name = `${this.spec.Name} - Prompt ${i + 1}`;
932
+ promptEntity.Description = `Agent prompt ${i + 1} for ${this.spec.Name}`;
933
+ promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
934
+ promptEntity.Status = 'Active';
935
+ promptEntity.ResponseFormat = 'JSON';
936
+ // Handle prompt text - supports both string and object formats
937
+ if (typeof promptSpec.PromptText === 'string') {
938
+ promptEntity.TemplateText = promptSpec.PromptText;
939
+ }
940
+ else if (typeof promptSpec.PromptText === 'object') {
941
+ // Architect may send PromptText as {text: "...", json: {...}}
942
+ const promptTextObj = promptSpec.PromptText;
943
+ let combinedText = promptTextObj.text || '';
944
+ if (promptTextObj.json) {
945
+ combinedText += '\n\n```json\n' + JSON.stringify(promptTextObj.json, null, 2) + '\n```';
946
+ }
947
+ promptEntity.TemplateText = combinedText;
948
+ }
949
+ // Set prompt role and position if provided
950
+ if (promptSpec.PromptRole) {
951
+ promptEntity.PromptRole = promptSpec.PromptRole;
952
+ }
953
+ if (promptSpec.PromptPosition) {
954
+ promptEntity.PromptPosition = promptSpec.PromptPosition;
955
+ }
956
+ const saved = await promptEntity.Save();
957
+ if (!saved) {
958
+ throw new Error(`Failed to save prompt ${i + 1} for agent ${this.spec.Name}`);
959
+ }
960
+ // Track mutation
961
+ this.trackMutation('AI Prompts', isUpdate ? 'Update' : 'Create', promptEntity.ID, `${isUpdate ? 'Updated' : 'Created'} prompt: ${promptEntity.Name}`);
962
+ // Update the spec with the saved/updated ID
963
+ promptSpec.PromptID = promptEntity.ID;
964
+ console.log(`✅ savePrompts: ${isUpdate ? 'Updated' : 'Created'} AIPrompt with ID: ${promptEntity.ID}`);
965
+ // Step 2: Create or update AIAgentPrompt junction
966
+ // Query for existing junction by AgentID + PromptID
967
+ const existingJunctionResult = await rv.RunView({
968
+ EntityName: 'MJ: AI Agent Prompts',
969
+ ExtraFilter: `AgentID='${agentId}' AND PromptID='${promptEntity.ID}'`,
970
+ ResultType: 'entity_object'
971
+ }, this._contextUser);
972
+ let agentPromptEntity;
973
+ let isJunctionUpdate = false;
974
+ if (existingJunctionResult.Success && existingJunctionResult.Results && existingJunctionResult.Results.length > 0) {
975
+ // Junction exists - load it for update
976
+ agentPromptEntity = existingJunctionResult.Results[0];
977
+ isJunctionUpdate = true;
978
+ console.log(`💬 savePrompts: Updating existing junction for prompt ${i + 1}`);
979
+ }
980
+ else {
981
+ // Junction doesn't exist - create new one
982
+ agentPromptEntity = await md.GetEntityObject('MJ: AI Agent Prompts', this._contextUser);
983
+ agentPromptEntity.AgentID = agentId;
984
+ agentPromptEntity.PromptID = promptEntity.ID;
985
+ console.log(`💬 savePrompts: Creating new junction for prompt ${i + 1}`);
986
+ }
987
+ // Set/update common fields (ExecutionOrder might change)
988
+ agentPromptEntity.ExecutionOrder = i;
989
+ agentPromptEntity.Status = 'Active';
990
+ const junctionSaved = await agentPromptEntity.Save();
991
+ if (!junctionSaved) {
992
+ throw new Error(`Failed to save agent-prompt junction for prompt ${i + 1}`);
993
+ }
994
+ // Track mutation
995
+ this.trackMutation('AI Agent Prompts', isJunctionUpdate ? 'Update' : 'Create', agentPromptEntity.ID, `${isJunctionUpdate ? 'Updated' : 'Created'} agent-prompt junction for prompt ${promptEntity.ID}`);
996
+ console.log(`✅ savePrompts: ${isJunctionUpdate ? 'Updated' : 'Created'} AIAgentPrompt junction with ID: ${agentPromptEntity.ID}`);
997
+ }
998
+ console.log(`✅ savePrompts: Successfully saved all ${this.spec.Prompts.length} prompt(s)`);
999
+ }
1000
+ /**
1001
+ * Save all steps for this agent (Flow agents only).
1002
+ *
1003
+ * Creates AIAgentStep records for each step in the spec. Steps define the nodes
1004
+ * in a Flow agent's execution graph.
1005
+ *
1006
+ * @private
1007
+ * @param agentId - The parent agent ID
1008
+ * @throws {Error} If any step save fails
1009
+ */
1010
+ async saveSteps(agentId) {
1011
+ console.log(`🔷 saveSteps: Called with agentId=${agentId}, Steps=${this.spec.Steps ? this.spec.Steps.length : 'undefined'}`);
1012
+ if (!this.spec.Steps || this.spec.Steps.length === 0) {
1013
+ console.log('🔷 saveSteps: No steps to save, returning early');
1014
+ return;
1015
+ }
1016
+ console.log(`🔷 saveSteps: Processing ${this.spec.Steps.length} step(s)...`);
1017
+ const md = new core_1.Metadata();
1018
+ for (const stepSpec of this.spec.Steps) {
1019
+ const stepEntity = await md.GetEntityObject('MJ: AI Agent Steps', this._contextUser);
1020
+ // Load existing if ID provided
1021
+ const isUpdate = !!(stepSpec.ID && stepSpec.ID !== '');
1022
+ if (isUpdate) {
1023
+ await stepEntity.Load(stepSpec.ID);
1024
+ }
1025
+ // Map fields
1026
+ stepEntity.AgentID = agentId;
1027
+ stepEntity.Name = stepSpec.Name;
1028
+ stepEntity.Description = stepSpec.Description || null;
1029
+ stepEntity.StepType = stepSpec.StepType;
1030
+ stepEntity.StartingStep = stepSpec.StartingStep;
1031
+ stepEntity.ActionID = stepSpec.ActionID || null;
1032
+ stepEntity.SubAgentID = stepSpec.SubAgentID || null;
1033
+ stepEntity.Status = 'Active'; // Default to Active
1034
+ // Handle inline prompt creation for Prompt-type steps
1035
+ // If StepType is Prompt and PromptID is empty, create a new AIPrompt record
1036
+ if (stepSpec.StepType === 'Prompt' && (!stepSpec.PromptID || stepSpec.PromptID === '')) {
1037
+ if (stepSpec.PromptText) {
1038
+ console.log(`🔷 saveSteps: Creating inline prompt for step "${stepSpec.Name}"`);
1039
+ // Create AIPrompt entity (using any type for TemplateText dynamic property)
1040
+ const promptEntity = await md.GetEntityObject('AI Prompts', this._contextUser);
1041
+ // Set prompt fields
1042
+ promptEntity.Name = stepSpec.PromptName || `${stepSpec.Name} Prompt`;
1043
+ promptEntity.Description = stepSpec.PromptDescription || `Prompt for step: ${stepSpec.Name}`;
1044
+ promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
1045
+ promptEntity.Status = 'Active';
1046
+ promptEntity.ResponseFormat = 'JSON';
1047
+ promptEntity.TemplateText = stepSpec.PromptText; // TemplateText is a dynamic property
1048
+ const promptSaved = await promptEntity.Save();
1049
+ if (!promptSaved) {
1050
+ throw new Error(`Failed to save inline prompt for step "${stepSpec.Name}"`);
1051
+ }
1052
+ // Track mutation
1053
+ this.trackMutation('AI Prompts', 'Create', promptEntity.ID, `Created inline prompt: ${promptEntity.Name}`);
1054
+ console.log(`✅ saveSteps: Created inline AIPrompt with ID: ${promptEntity.ID}`);
1055
+ // Link the created prompt to this step
1056
+ stepEntity.PromptID = promptEntity.ID;
1057
+ stepSpec.PromptID = promptEntity.ID; // Update spec too
1058
+ }
1059
+ else {
1060
+ console.warn(`⚠️ saveSteps: Step "${stepSpec.Name}" is Prompt type with empty PromptID but no PromptText provided`);
1061
+ stepEntity.PromptID = null;
1062
+ }
1063
+ }
1064
+ else {
1065
+ stepEntity.PromptID = stepSpec.PromptID || null;
1066
+ }
1067
+ // Map Action I/O mappings for Flow agent action steps
1068
+ // Handle ActionInputMapping - convert object to JSON string if needed
1069
+ if (stepSpec.ActionInputMapping) {
1070
+ stepEntity.ActionInputMapping = typeof stepSpec.ActionInputMapping === 'string'
1071
+ ? stepSpec.ActionInputMapping
1072
+ : JSON.stringify(stepSpec.ActionInputMapping);
1073
+ }
1074
+ else {
1075
+ stepEntity.ActionInputMapping = null;
1076
+ }
1077
+ // Handle ActionOutputMapping - convert object to JSON string if needed
1078
+ if (stepSpec.ActionOutputMapping) {
1079
+ stepEntity.ActionOutputMapping = typeof stepSpec.ActionOutputMapping === 'string'
1080
+ ? stepSpec.ActionOutputMapping
1081
+ : JSON.stringify(stepSpec.ActionOutputMapping);
1082
+ }
1083
+ else {
1084
+ stepEntity.ActionOutputMapping = null;
1085
+ }
1086
+ const saved = await stepEntity.Save();
1087
+ if (!saved) {
1088
+ throw new Error(`Failed to save step "${stepSpec.Name}" for agent ${this.spec.Name}`);
1089
+ }
1090
+ // Track mutation
1091
+ this.trackMutation('AI Agent Steps', isUpdate ? 'Update' : 'Create', stepEntity.ID, `${isUpdate ? 'Updated' : 'Created'} step: ${stepSpec.Name}`);
1092
+ // Update the spec with the saved ID so paths can reference it
1093
+ stepSpec.ID = stepEntity.ID;
1094
+ console.log(`✅ saveSteps: Created AIAgentStep with ID: ${stepEntity.ID}`);
1095
+ }
1096
+ console.log(`✅ saveSteps: Successfully saved all ${this.spec.Steps.length} step(s)`);
1097
+ }
1098
+ /**
1099
+ * Save all step paths for this agent (Flow agents only).
1100
+ *
1101
+ * Creates AIAgentStepPath records that define the edges in a Flow agent's
1102
+ * execution graph, including conditional branching logic.
1103
+ *
1104
+ * @private
1105
+ * @param agentId - The parent agent ID
1106
+ * @throws {Error} If any path save fails
1107
+ */
1108
+ async saveStepPaths(agentId) {
1109
+ console.log(`🔶 saveStepPaths: Called with agentId=${agentId}, Paths=${this.spec.Paths ? this.spec.Paths.length : 'undefined'}`);
1110
+ if (!this.spec.Paths || this.spec.Paths.length === 0) {
1111
+ console.log('🔶 saveStepPaths: No paths to save, returning early');
1112
+ return;
1113
+ }
1114
+ console.log(`🔶 saveStepPaths: Processing ${this.spec.Paths.length} path(s)...`);
1115
+ const md = new core_1.Metadata();
1116
+ // Create a map of step names to IDs for easy lookup
1117
+ const stepNameToId = new Map();
1118
+ if (this.spec.Steps) {
1119
+ for (const step of this.spec.Steps) {
1120
+ if (step.ID) {
1121
+ stepNameToId.set(step.Name, step.ID);
1122
+ }
1123
+ }
1124
+ }
1125
+ for (const pathSpec of this.spec.Paths) {
1126
+ const pathEntity = await md.GetEntityObject('MJ: AI Agent Step Paths', this._contextUser);
1127
+ // Load existing if ID provided
1128
+ const isUpdate = !!(pathSpec.ID && pathSpec.ID !== '');
1129
+ if (isUpdate) {
1130
+ await pathEntity.Load(pathSpec.ID);
1131
+ }
1132
+ // Resolve step IDs from step names
1133
+ // OriginStepID and DestinationStepID can be either step IDs or step names
1134
+ // If they're names, look them up in our map
1135
+ let originStepId = pathSpec.OriginStepID;
1136
+ let destinationStepId = pathSpec.DestinationStepID;
1137
+ // Try to resolve as step names first
1138
+ if (stepNameToId.has(pathSpec.OriginStepID)) {
1139
+ originStepId = stepNameToId.get(pathSpec.OriginStepID);
1140
+ }
1141
+ if (stepNameToId.has(pathSpec.DestinationStepID)) {
1142
+ destinationStepId = stepNameToId.get(pathSpec.DestinationStepID);
1143
+ }
1144
+ // Map fields
1145
+ pathEntity.OriginStepID = originStepId;
1146
+ pathEntity.DestinationStepID = destinationStepId;
1147
+ pathEntity.Condition = pathSpec.Condition || null;
1148
+ pathEntity.Description = pathSpec.Description || null;
1149
+ pathEntity.Priority = pathSpec.Priority;
1150
+ const saved = await pathEntity.Save();
1151
+ if (!saved) {
1152
+ throw new Error(`Failed to save path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`);
1153
+ }
1154
+ // Track mutation
1155
+ this.trackMutation('AI Agent Step Paths', isUpdate ? 'Update' : 'Create', pathEntity.ID, `${isUpdate ? 'Updated' : 'Created'} path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`);
1156
+ // Update the spec with the saved ID
1157
+ pathSpec.ID = pathEntity.ID;
1158
+ console.log(`✅ saveStepPaths: Created AIAgentStepPath with ID: ${pathEntity.ID}`);
1159
+ }
1160
+ console.log(`✅ saveStepPaths: Successfully saved all ${this.spec.Paths.length} path(s)`);
1161
+ }
622
1162
  // ===== UTILITY METHODS =====
623
1163
  /**
624
1164
  * Parse a JSON string field, returning undefined if null/empty.
@@ -656,6 +1196,9 @@ class AgentSpecSync {
656
1196
  ID: partial.ID || '',
657
1197
  Name: partial.Name || '',
658
1198
  Description: partial.Description,
1199
+ // TypeID and Status are extended fields (not in base AgentSpec interface)
1200
+ TypeID: partial.TypeID,
1201
+ Status: partial.Status || 'Active',
659
1202
  IconClass: partial.IconClass,
660
1203
  LogoURL: partial.LogoURL,
661
1204
  ParentID: partial.ParentID,
@@ -676,14 +1219,20 @@ class AgentSpecSync {
676
1219
  MinExecutionsPerRun: partial.MinExecutionsPerRun,
677
1220
  MaxExecutionsPerRun: partial.MaxExecutionsPerRun,
678
1221
  StartingPayloadValidation: partial.StartingPayloadValidation,
679
- StartingPayloadValidationMode: partial.StartingPayloadValidationMode || (() => 'Fail'),
1222
+ StartingPayloadValidationMode: partial.StartingPayloadValidationMode || 'Fail',
680
1223
  DefaultPromptEffortLevel: partial.DefaultPromptEffortLevel,
681
1224
  ChatHandlingOption: partial.ChatHandlingOption,
682
1225
  DefaultArtifactTypeID: partial.DefaultArtifactTypeID,
683
1226
  OwnerUserID: partial.OwnerUserID,
684
1227
  InvocationMode: partial.InvocationMode || 'Any',
1228
+ FunctionalRequirements: partial.FunctionalRequirements,
1229
+ TechnicalDesign: partial.TechnicalDesign,
685
1230
  Actions: partial.Actions || [],
686
- SubAgents: partial.SubAgents || []
1231
+ SubAgents: partial.SubAgents || [],
1232
+ Prompts: partial.Prompts || [],
1233
+ // Flow agent fields - critical for Flow agent support
1234
+ Steps: partial.Steps || [],
1235
+ Paths: partial.Paths || []
687
1236
  };
688
1237
  }
689
1238
  /**