@memberjunction/ai-agent-manager 2.109.0 → 2.110.1

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.
@@ -7,7 +7,9 @@ import {
7
7
  import {
8
8
  AIAgentEntity,
9
9
  AIAgentActionEntity,
10
- AIAgentRelationshipEntity
10
+ AIAgentRelationshipEntity,
11
+ AIAgentStepEntity,
12
+ AIAgentStepPathEntity
11
13
  } from '@memberjunction/core-entities';
12
14
  import {
13
15
  AgentSpec,
@@ -15,6 +17,32 @@ import {
15
17
  SubAgentSpec
16
18
  } from '@memberjunction/ai-core-plus';
17
19
 
20
+ /**
21
+ * Represents a single database mutation performed by AgentSpecSync
22
+ */
23
+ export interface AgentSpecSyncMutation {
24
+ /** Entity name (e.g., "AI Agents", "AI Prompts", "AIAgentAction") */
25
+ Entity: string;
26
+ /** Operation type */
27
+ Operation: 'Create' | 'Update' | 'Delete';
28
+ /** Record ID */
29
+ ID: string;
30
+ /** Human-readable description of what was done */
31
+ Description: string;
32
+ }
33
+
34
+ /**
35
+ * Result of SaveToDatabase operation including all mutations performed
36
+ */
37
+ export interface AgentSpecSyncResult {
38
+ /** ID of the saved agent */
39
+ agentId: string;
40
+ /** Whether the operation succeeded */
41
+ success: boolean;
42
+ /** Array of all database mutations performed */
43
+ mutations: AgentSpecSyncMutation[];
44
+ }
45
+
18
46
  /**
19
47
  * AgentSpecSync provides a high-level interface for working with AI Agent metadata in MemberJunction.
20
48
  *
@@ -89,6 +117,12 @@ export class AgentSpecSync {
89
117
  */
90
118
  private _contextUser?: UserInfo;
91
119
 
120
+ /**
121
+ * Tracks all database mutations performed during save operations
122
+ * @private
123
+ */
124
+ private _mutations: AgentSpecSyncMutation[] = [];
125
+
92
126
  /**
93
127
  * Create a new AgentSpecSync instance.
94
128
  *
@@ -117,6 +151,41 @@ export class AgentSpecSync {
117
151
  this._contextUser = contextUser;
118
152
  }
119
153
 
154
+ // ===== MUTATION TRACKING METHODS =====
155
+
156
+ /**
157
+ * Track a database mutation performed during save operation
158
+ * @private
159
+ */
160
+ private trackMutation(
161
+ entity: string,
162
+ operation: 'Create' | 'Update' | 'Delete',
163
+ id: string,
164
+ description: string
165
+ ): void {
166
+ this._mutations.push({
167
+ Entity: entity,
168
+ Operation: operation,
169
+ ID: id,
170
+ Description: description
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Get all mutations tracked during the last save operation
176
+ * @returns Array of mutations
177
+ */
178
+ public getMutations(): AgentSpecSyncMutation[] {
179
+ return [...this._mutations];
180
+ }
181
+
182
+ /**
183
+ * Clear all tracked mutations
184
+ */
185
+ public clearMutations(): void {
186
+ this._mutations = [];
187
+ }
188
+
120
189
  // ===== STATIC FACTORY METHODS =====
121
190
 
122
191
  /**
@@ -256,7 +325,8 @@ export class AgentSpecSync {
256
325
  }
257
326
 
258
327
  // Step 2: Batch load related entities using RunViews for optimal performance
259
- const [actionsResult, childAgentsResult, relatedAgentsResult] = await rv.RunViews([
328
+ // Note: Load paths separately after steps to avoid hardcoded view name in subquery
329
+ const [actionsResult, childAgentsResult, relatedAgentsResult, promptsResult, stepsResult] = await rv.RunViews([
260
330
  {
261
331
  EntityName: 'AI Agent Actions',
262
332
  ExtraFilter: `AgentID='${agentId}'`,
@@ -274,6 +344,18 @@ export class AgentSpecSync {
274
344
  ExtraFilter: `AgentID='${agentId}' AND Status='Active'`,
275
345
  OrderBy: '__mj_CreatedAt',
276
346
  ResultType: 'entity_object'
347
+ },
348
+ {
349
+ EntityName: 'MJ: AI Agent Prompts',
350
+ ExtraFilter: `AgentID='${agentId}'`,
351
+ OrderBy: 'ExecutionOrder',
352
+ ResultType: 'entity_object'
353
+ },
354
+ {
355
+ EntityName: 'MJ: AI Agent Steps',
356
+ ExtraFilter: `AgentID='${agentId}'`,
357
+ OrderBy: 'Name',
358
+ ResultType: 'entity_object'
277
359
  }
278
360
  ], this._contextUser);
279
361
 
@@ -287,20 +369,101 @@ export class AgentSpecSync {
287
369
  if (!relatedAgentsResult.Success) {
288
370
  throw new Error(`Failed to load related agents: ${relatedAgentsResult.ErrorMessage}`);
289
371
  }
372
+ if (!promptsResult.Success) {
373
+ throw new Error(`Failed to load agent prompts: ${promptsResult.ErrorMessage}`);
374
+ }
375
+ if (!stepsResult.Success) {
376
+ throw new Error(`Failed to load agent steps: ${stepsResult.ErrorMessage}`);
377
+ }
378
+
379
+ // Step 2b: Load paths separately using step IDs to avoid hardcoded view name
380
+ let pathsResult;
381
+ const steps = stepsResult.Results || [];
382
+ if (steps.length > 0) {
383
+ const stepIds = steps.map((s: AIAgentStepEntity) => `'${s.ID}'`).join(',');
384
+ pathsResult = await rv.RunView<AIAgentStepPathEntity>({
385
+ EntityName: 'MJ: AI Agent Step Paths',
386
+ ExtraFilter: `OriginStepID IN (${stepIds})`,
387
+ OrderBy: 'Priority DESC',
388
+ ResultType: 'entity_object'
389
+ }, this._contextUser);
390
+
391
+ if (!pathsResult.Success) {
392
+ throw new Error(`Failed to load step paths: ${pathsResult.ErrorMessage}`);
393
+ }
394
+ } else {
395
+ // No steps, so no paths
396
+ pathsResult = {
397
+ Success: true,
398
+ Results: [],
399
+ RowCount: 0
400
+ };
401
+ }
402
+
403
+ // Step 2c: Load full AI Prompt records to get PromptText, PromptRole, PromptPosition
404
+ let fullPromptsResult;
405
+ const agentPrompts = promptsResult.Results || [];
406
+ if (agentPrompts.length > 0) {
407
+ const promptIds = agentPrompts.map((p: any) => `'${p.PromptID}'`).join(',');
408
+ fullPromptsResult = await rv.RunView({
409
+ EntityName: 'AI Prompts',
410
+ ExtraFilter: `ID IN (${promptIds})`,
411
+ OrderBy: 'Name',
412
+ ResultType: 'entity_object'
413
+ }, this._contextUser);
414
+
415
+ if (!fullPromptsResult.Success) {
416
+ throw new Error(`Failed to load AI prompts: ${fullPromptsResult.ErrorMessage}`);
417
+ }
418
+ } else {
419
+ // No prompts
420
+ fullPromptsResult = {
421
+ Success: true,
422
+ Results: [],
423
+ RowCount: 0
424
+ };
425
+ }
290
426
 
291
427
  // Step 3: Map entities to raw spec format
292
428
  this.spec = this.mapEntitiesToRawSpec(
293
429
  agentEntity,
294
430
  actionsResult.Results || [],
295
431
  childAgentsResult.Results || [],
296
- relatedAgentsResult.Results || []
432
+ relatedAgentsResult.Results || [],
433
+ promptsResult.Results || [],
434
+ fullPromptsResult.Results || [],
435
+ stepsResult.Results || [],
436
+ pathsResult.Results || []
297
437
  );
298
438
 
299
439
  // Step 4: Recursively load sub-agents if requested
300
440
  if (includeSubAgents && this.spec.SubAgents && this.spec.SubAgents.length > 0) {
301
- // Note: We don't recursively populate the full spec here since SubAgentSpec
302
- // only contains the ID and relationship metadata. To get full details,
303
- // users can call LoadFromDatabase on the SubAgentID separately if needed.
441
+ // Recursively load complete specs for all sub-agents
442
+ for (const subAgentSpec of this.spec.SubAgents) {
443
+ if (subAgentSpec.SubAgent && subAgentSpec.SubAgent.ID) {
444
+ try {
445
+ // Load the complete sub-agent spec recursively
446
+ const subAgentSync = await AgentSpecSync.LoadFromDatabase(
447
+ subAgentSpec.SubAgent.ID,
448
+ this._contextUser,
449
+ true // Recursively load nested sub-agents too
450
+ );
451
+
452
+ // Replace the minimal SubAgent with the complete spec
453
+ const fullSubAgentSpec = subAgentSync.toJSON();
454
+
455
+ // Preserve the relationship metadata while adding full spec details
456
+ subAgentSpec.SubAgent = {
457
+ ...fullSubAgentSpec,
458
+ // Ensure we preserve any relationship-specific overrides
459
+ StartingPayloadValidationMode: subAgentSpec.SubAgent.StartingPayloadValidationMode || fullSubAgentSpec.StartingPayloadValidationMode
460
+ };
461
+ } catch (error) {
462
+ LogError(`Failed to load sub-agent ${subAgentSpec.SubAgent.ID}: ${error instanceof Error ? error.message : String(error)}`);
463
+ // Continue with partial data rather than failing completely
464
+ }
465
+ }
466
+ }
304
467
  }
305
468
 
306
469
  this._isLoaded = true;
@@ -318,19 +481,29 @@ export class AgentSpecSync {
318
481
  * @param actions - Array of agent action entities
319
482
  * @param childAgents - Array of child agent entities (ParentID-based)
320
483
  * @param relatedAgents - Array of related agent relationship entities
484
+ * @param agentPrompts - Array of agent prompt junction entities
485
+ * @param fullPrompts - Array of full AI Prompt entities with PromptText, PromptRole, PromptPosition
486
+ * @param steps - Array of agent step entities (for Flow agents)
487
+ * @param paths - Array of step path entities (for Flow agents)
321
488
  * @returns Fully populated AgentSpec object
322
489
  */
323
490
  private mapEntitiesToRawSpec(
324
491
  agent: AIAgentEntity,
325
492
  actions: AIAgentActionEntity[],
326
493
  childAgents: AIAgentEntity[],
327
- relatedAgents: AIAgentRelationshipEntity[]
494
+ relatedAgents: AIAgentRelationshipEntity[],
495
+ agentPrompts: any[],
496
+ fullPrompts: any[],
497
+ steps: AIAgentStepEntity[],
498
+ paths: AIAgentStepPathEntity[]
328
499
  ): AgentSpec {
329
500
  // Map all agent fields to spec
330
501
  const spec: AgentSpec = {
331
502
  ID: agent.ID,
332
503
  Name: agent.Name || '',
333
504
  Description: agent.Description || undefined,
505
+ TypeID: agent.TypeID || undefined,
506
+ Status: (agent.Status as 'Active' | 'Inactive' | 'Pending') || undefined,
334
507
  IconClass: agent.IconClass || undefined,
335
508
  LogoURL: agent.LogoURL || undefined,
336
509
  ParentID: agent.ParentID || undefined,
@@ -369,6 +542,10 @@ export class AgentSpecSync {
369
542
  OwnerUserID: agent.OwnerUserID || undefined,
370
543
  InvocationMode: agent.InvocationMode,
371
544
 
545
+ // Requirements and design documentation
546
+ FunctionalRequirements: (agent as any).FunctionalRequirements || null,
547
+ TechnicalDesign: (agent as any).TechnicalDesign || null,
548
+
372
549
  // Map actions
373
550
  Actions: actions.map(action => this.mapActionEntityToSpec(action)),
374
551
 
@@ -376,7 +553,16 @@ export class AgentSpecSync {
376
553
  SubAgents: [
377
554
  ...childAgents.map(child => this.mapChildAgentToSpec(child)),
378
555
  ...relatedAgents.map(rel => this.mapRelatedAgentToSpec(rel))
379
- ]
556
+ ],
557
+
558
+ // Map prompts (agent-level prompts for Loop agents)
559
+ Prompts: agentPrompts.map(agentPrompt => this.mapPromptEntityToSpec(agentPrompt, fullPrompts)),
560
+
561
+ // Map steps (for Flow agents)
562
+ Steps: steps.map(step => this.mapStepEntityToSpec(step)),
563
+
564
+ // Map paths (for Flow agents)
565
+ Paths: paths.map(path => this.mapPathEntityToSpec(path))
380
566
  };
381
567
 
382
568
  return spec;
@@ -443,6 +629,76 @@ export class AgentSpecSync {
443
629
  };
444
630
  }
445
631
 
632
+ /**
633
+ * Map AIAgentPromptEntity junction to AgentPromptSpec format.
634
+ *
635
+ * @private
636
+ * @param agentPrompt - The agent prompt junction entity (has PromptID, ExecutionOrder)
637
+ * @param fullPrompts - Array of full AI Prompt entities
638
+ * @returns Mapped prompt spec
639
+ */
640
+ private mapPromptEntityToSpec(agentPrompt: any, fullPrompts: any[]): any {
641
+ // Find the full prompt data by matching PromptID
642
+ const fullPrompt = fullPrompts.find((p: any) => p.ID === agentPrompt.PromptID);
643
+
644
+ if (!fullPrompt) {
645
+ // Fallback if prompt not found
646
+ return {
647
+ PromptID: agentPrompt.PromptID || '',
648
+ PromptText: '',
649
+ PromptRole: 'System',
650
+ PromptPosition: 'First'
651
+ };
652
+ }
653
+
654
+ return {
655
+ PromptID: fullPrompt.ID || '',
656
+ PromptText: fullPrompt.TemplateText || '',
657
+ PromptRole: fullPrompt.PromptRole || 'System',
658
+ PromptPosition: fullPrompt.PromptPosition || 'First'
659
+ };
660
+ }
661
+
662
+ /**
663
+ * Map AIAgentStepEntity to AgentStep format.
664
+ *
665
+ * @private
666
+ * @param step - The agent step entity
667
+ * @returns Mapped step spec
668
+ */
669
+ private mapStepEntityToSpec(step: AIAgentStepEntity): any {
670
+ return {
671
+ ID: step.ID,
672
+ Name: step.Name,
673
+ Description: step.Description || undefined,
674
+ StepType: step.StepType,
675
+ StartingStep: step.StartingStep,
676
+ ActionID: step.ActionID || undefined,
677
+ SubAgentID: step.SubAgentID || undefined,
678
+ PromptID: step.PromptID || undefined,
679
+ ActionInputMapping: this.parseJsonField<any>(step.ActionInputMapping),
680
+ ActionOutputMapping: this.parseJsonField<any>(step.ActionOutputMapping)
681
+ };
682
+ }
683
+
684
+ /**
685
+ * Map AIAgentStepPathEntity to AgentStepPath format.
686
+ *
687
+ * @private
688
+ * @param path - The agent step path entity
689
+ * @returns Mapped path spec
690
+ */
691
+ private mapPathEntityToSpec(path: AIAgentStepPathEntity): any {
692
+ return {
693
+ ID: path.ID,
694
+ OriginStepID: path.OriginStepID,
695
+ DestinationStepID: path.DestinationStepID,
696
+ Condition: path.Condition || undefined,
697
+ Description: path.Description || undefined,
698
+ Priority: path.Priority
699
+ };
700
+ }
701
+
446
702
  // ===== SAVING METHODS =====
447
703
 
448
704
  /**
@@ -455,7 +711,7 @@ export class AgentSpecSync {
455
711
  * database schema are executed, and any failures will prevent the save.
456
712
  *
457
713
  * @param validate - Whether to validate before saving (default: true)
458
- * @returns Promise resolving to the saved agent ID
714
+ * @returns Promise resolving to result with agent ID, success flag, and all mutations performed
459
715
  * @throws {Error} If validation fails or save operation fails
460
716
  *
461
717
  * @example
@@ -465,32 +721,59 @@ export class AgentSpecSync {
465
721
  * InvocationMode: 'Any'
466
722
  * }, contextUser);
467
723
  *
468
- * const agentId = await spec.SaveToDatabase();
469
- * console.log('Saved with ID:', agentId);
724
+ * const result = await spec.SaveToDatabase();
725
+ * console.log('Saved with ID:', result.agentId);
726
+ * console.log('Mutations:', result.mutations.length);
470
727
  * ```
471
728
  */
472
- async SaveToDatabase(validate: boolean = true): Promise<string> {
729
+ async SaveToDatabase(validate: boolean = true): Promise<AgentSpecSyncResult> {
730
+ // Clear previous mutations
731
+ this.clearMutations();
732
+
473
733
  if (!this._isDirty && this._isLoaded) {
474
734
  // No changes to save
475
- return this.spec.ID;
735
+ return {
736
+ agentId: this.spec.ID,
737
+ success: true,
738
+ mutations: []
739
+ };
476
740
  }
477
741
 
478
- // Step 1: Save main agent entity
479
- const agentId = await this.saveAgentEntity(validate);
742
+ try {
743
+ // Step 1: Save main agent entity
744
+ const agentId = await this.saveAgentEntity(validate);
480
745
 
481
- // Step 2: Save actions
482
- await this.saveActions(agentId);
746
+ // Step 2: Save actions
747
+ await this.saveActions(agentId);
483
748
 
484
- // Step 3: Save sub-agents (both child and related)
485
- await this.saveSubAgents(agentId);
749
+ // Step 3: Save sub-agents (both child and related)
750
+ await this.saveSubAgents(agentId);
486
751
 
487
- // Step 4: Save prompts
488
- await this.savePrompts(agentId);
752
+ // Step 4: Save prompts
753
+ await this.savePrompts(agentId);
489
754
 
490
- this._isDirty = false;
491
- this._isLoaded = true;
755
+ // Step 5: Save steps (Flow agents only)
756
+ await this.saveSteps(agentId);
492
757
 
493
- return agentId;
758
+ // Step 6: Save step paths (Flow agents only)
759
+ await this.saveStepPaths(agentId);
760
+
761
+ this._isDirty = false;
762
+ this._isLoaded = true;
763
+
764
+ return {
765
+ agentId,
766
+ success: true,
767
+ mutations: this.getMutations()
768
+ };
769
+ } catch (error) {
770
+ // Return failure with whatever mutations were completed before error
771
+ return {
772
+ agentId: this.spec.ID || '',
773
+ success: false,
774
+ mutations: this.getMutations()
775
+ };
776
+ }
494
777
  }
495
778
 
496
779
  /**
@@ -508,6 +791,9 @@ export class AgentSpecSync {
508
791
  this._contextUser
509
792
  );
510
793
 
794
+ // Track if this is an update or create
795
+ const isUpdate = !!this.spec.ID;
796
+
511
797
  // If ID exists, load existing record
512
798
  if (this.spec.ID) {
513
799
  const loaded = await agentEntity.Load(this.spec.ID);
@@ -525,6 +811,18 @@ export class AgentSpecSync {
525
811
  agentEntity.DriverClass = this.spec.DriverClass || null;
526
812
  agentEntity.ModelSelectionMode = this.spec.ModelSelectionMode || 'Agent Type';
527
813
 
814
+ // Handle TypeID - supports lookup references or direct GUID
815
+ if ((this.spec as any).TypeID) {
816
+ agentEntity.TypeID = (this.spec as any).TypeID;
817
+ }
818
+
819
+ // Handle Status - defaults to Active if not specified
820
+ if ((this.spec as any).Status) {
821
+ agentEntity.Status = (this.spec as any).Status;
822
+ } else {
823
+ agentEntity.Status = 'Active';
824
+ }
825
+
528
826
  // Serialize JSON fields
529
827
  agentEntity.PayloadDownstreamPaths = JSON.stringify(
530
828
  this.spec.PayloadDownstreamPaths || ['*']
@@ -567,6 +865,10 @@ export class AgentSpecSync {
567
865
  }
568
866
  agentEntity.InvocationMode = this.spec.InvocationMode || 'Any';
569
867
 
868
+ // Requirements and design documentation
869
+ agentEntity.FunctionalRequirements = this.spec.FunctionalRequirements || null;
870
+ agentEntity.TechnicalDesign = this.spec.TechnicalDesign || null;
871
+
570
872
  // Validate if requested
571
873
  if (validate) {
572
874
  const validation = agentEntity.Validate();
@@ -584,6 +886,15 @@ export class AgentSpecSync {
584
886
 
585
887
  // Update spec with saved ID
586
888
  this.spec.ID = agentEntity.ID;
889
+
890
+ // Track the mutation
891
+ this.trackMutation(
892
+ 'AI Agents',
893
+ isUpdate ? 'Update' : 'Create',
894
+ agentEntity.ID,
895
+ `${isUpdate ? 'Updated' : 'Created'} agent: ${this.spec.Name}`
896
+ );
897
+
587
898
  return agentEntity.ID;
588
899
  }
589
900
 
@@ -611,6 +922,9 @@ export class AgentSpecSync {
611
922
  this._contextUser
612
923
  );
613
924
 
925
+ // Track if this is an update or create
926
+ const isUpdate = !!actionSpec.AgentActionID;
927
+
614
928
  // Load existing if ID provided
615
929
  if (actionSpec.AgentActionID) {
616
930
  await actionEntity.Load(actionSpec.AgentActionID);
@@ -634,6 +948,14 @@ export class AgentSpecSync {
634
948
 
635
949
  // Update spec with saved ID
636
950
  actionSpec.AgentActionID = actionEntity.ID;
951
+
952
+ // Track the mutation
953
+ this.trackMutation(
954
+ 'AI Agent Actions',
955
+ isUpdate ? 'Update' : 'Create',
956
+ actionEntity.ID,
957
+ `${isUpdate ? 'Updated' : 'Created'} action junction for action ${actionSpec.ActionID}`
958
+ );
637
959
  }
638
960
  }
639
961
 
@@ -693,38 +1015,29 @@ export class AgentSpecSync {
693
1015
  // Recursively create the sub-agent using AgentSpecSync
694
1016
  const childSync = new AgentSpecSync(childSpec, this._contextUser);
695
1017
  childSync.markDirty();
696
- const childId = await childSync.SaveToDatabase();
1018
+ const childResult = await childSync.SaveToDatabase();
697
1019
 
698
1020
  // Update the SubAgentSpec with the created ID so parent can reference it
699
- SubAgentSpec.SubAgent.ID = childId;
1021
+ SubAgentSpec.SubAgent.ID = childResult.agentId;
700
1022
 
701
- console.log(`✅ saveChildSubAgent: Created child sub-agent with ID: ${childId}`);
1023
+ console.log(`✅ saveChildSubAgent: Created child sub-agent with ID: ${childResult.agentId}`);
702
1024
  } else {
703
- // SubAgent already exists - just ensure ParentID is set correctly
1025
+ // SubAgent already exists - update it recursively to capture all field changes
704
1026
  console.log(`🔗 saveChildSubAgent: Updating existing child sub-agent "${SubAgentSpec.SubAgent.ID}"...`);
705
1027
 
706
- const md = new Metadata();
707
- const childEntity = await md.GetEntityObject<AIAgentEntity>(
708
- 'AI Agents',
709
- this._contextUser
710
- );
1028
+ // Set ParentID in the sub-agent spec
1029
+ const childSpec: AgentSpec = {
1030
+ ...SubAgentSpec.SubAgent,
1031
+ ParentID: parentId
1032
+ };
711
1033
 
712
- const loaded = await childEntity.Load(SubAgentSpec.SubAgent.ID);
713
- if (!loaded) {
714
- throw new Error(`Child agent ${SubAgentSpec.SubAgent.ID} not found in database`);
715
- }
1034
+ // Recursively update the sub-agent using AgentSpecSync
1035
+ // This ensures all fields including FunctionalRequirements and TechnicalDesign are updated
1036
+ const childSync = new AgentSpecSync(childSpec, this._contextUser);
1037
+ childSync.markDirty();
1038
+ await childSync.SaveToDatabase();
716
1039
 
717
- // Update parent ID if needed
718
- if (childEntity.ParentID !== parentId) {
719
- console.log(`🔗 saveChildSubAgent: Setting ParentID to ${parentId}`);
720
- childEntity.ParentID = parentId;
721
- const saved = await childEntity.Save();
722
- if (!saved) {
723
- throw new Error(`Failed to update ParentID for child agent ${SubAgentSpec.SubAgent.ID}`);
724
- }
725
- } else {
726
- console.log(`✅ saveChildSubAgent: ParentID already correct`);
727
- }
1040
+ console.log(`✅ saveChildSubAgent: Updated existing child sub-agent`);
728
1041
  }
729
1042
  }
730
1043
 
@@ -747,7 +1060,8 @@ export class AgentSpecSync {
747
1060
  );
748
1061
 
749
1062
  // Load existing if ID provided
750
- if (SubAgentSpec.AgentRelationshipID) {
1063
+ const isUpdate = !!SubAgentSpec.AgentRelationshipID;
1064
+ if (isUpdate && SubAgentSpec.AgentRelationshipID) {
751
1065
  await relationshipEntity.Load(SubAgentSpec.AgentRelationshipID);
752
1066
  }
753
1067
 
@@ -780,6 +1094,14 @@ export class AgentSpecSync {
780
1094
  throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`);
781
1095
  }
782
1096
 
1097
+ // Track mutation
1098
+ this.trackMutation(
1099
+ 'AI Agent Relationships',
1100
+ isUpdate ? 'Update' : 'Create',
1101
+ relationshipEntity.ID,
1102
+ `${isUpdate ? 'Updated' : 'Created'} agent relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`
1103
+ );
1104
+
783
1105
  // Update spec with saved ID
784
1106
  SubAgentSpec.AgentRelationshipID = relationshipEntity.ID;
785
1107
  }
@@ -805,17 +1127,31 @@ export class AgentSpecSync {
805
1127
 
806
1128
  console.log(`💬 savePrompts: Processing ${this.spec.Prompts.length} prompt(s)...`);
807
1129
  const md = new Metadata();
1130
+ const rv = new RunView();
808
1131
 
1132
+ // Step 1: Create or update AIPrompt records
809
1133
  for (let i = 0; i < this.spec.Prompts.length; i++) {
810
1134
  const promptSpec = this.spec.Prompts[i];
811
1135
 
812
- // Create AIPrompt entity
813
- const promptEntity = await md.GetEntityObject<any>(
1136
+ // Check if this is an update (has PromptID) or create (no PromptID)
1137
+ let promptEntity = await md.GetEntityObject<any>(
814
1138
  'AI Prompts',
815
1139
  this._contextUser
816
1140
  );
817
1141
 
818
- // Set required fields
1142
+ let isUpdate = false;
1143
+ if ((promptSpec as any).PromptID && (promptSpec as any).PromptID !== '') {
1144
+ // Load existing prompt for update
1145
+ const loaded = await promptEntity.Load((promptSpec as any).PromptID);
1146
+ if (loaded) {
1147
+ isUpdate = true;
1148
+ console.log(`💬 savePrompts: Updating existing prompt ID: ${(promptSpec as any).PromptID}`);
1149
+ } else {
1150
+ console.warn(`💬 savePrompts: PromptID specified but not found, creating new prompt`);
1151
+ }
1152
+ }
1153
+
1154
+ // Set all required fields (both create and update)
819
1155
  promptEntity.Name = `${this.spec.Name} - Prompt ${i + 1}`;
820
1156
  promptEntity.Description = `Agent prompt ${i + 1} for ${this.spec.Name}`;
821
1157
  promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
@@ -848,16 +1184,47 @@ export class AgentSpecSync {
848
1184
  throw new Error(`Failed to save prompt ${i + 1} for agent ${this.spec.Name}`);
849
1185
  }
850
1186
 
851
- console.log(`✅ savePrompts: Created AIPrompt with ID: ${promptEntity.ID}`);
852
-
853
- // Create AIAgentPrompt junction
854
- const agentPromptEntity = await md.GetEntityObject<any>(
855
- 'MJ: AI Agent Prompts',
856
- this._contextUser
1187
+ // Track mutation
1188
+ this.trackMutation(
1189
+ 'AI Prompts',
1190
+ isUpdate ? 'Update' : 'Create',
1191
+ promptEntity.ID,
1192
+ `${isUpdate ? 'Updated' : 'Created'} prompt: ${promptEntity.Name}`
857
1193
  );
858
1194
 
859
- agentPromptEntity.AgentID = agentId;
860
- agentPromptEntity.PromptID = promptEntity.ID;
1195
+ // Update the spec with the saved/updated ID
1196
+ (promptSpec as any).PromptID = promptEntity.ID;
1197
+
1198
+ console.log(`✅ savePrompts: ${isUpdate ? 'Updated' : 'Created'} AIPrompt with ID: ${promptEntity.ID}`);
1199
+
1200
+ // Step 2: Create or update AIAgentPrompt junction
1201
+ // Query for existing junction by AgentID + PromptID
1202
+ const existingJunctionResult = await rv.RunView<any>({
1203
+ EntityName: 'MJ: AI Agent Prompts',
1204
+ ExtraFilter: `AgentID='${agentId}' AND PromptID='${promptEntity.ID}'`,
1205
+ ResultType: 'entity_object'
1206
+ }, this._contextUser);
1207
+
1208
+ let agentPromptEntity: any;
1209
+ let isJunctionUpdate = false;
1210
+
1211
+ if (existingJunctionResult.Success && existingJunctionResult.Results && existingJunctionResult.Results.length > 0) {
1212
+ // Junction exists - load it for update
1213
+ agentPromptEntity = existingJunctionResult.Results[0];
1214
+ isJunctionUpdate = true;
1215
+ console.log(`💬 savePrompts: Updating existing junction for prompt ${i + 1}`);
1216
+ } else {
1217
+ // Junction doesn't exist - create new one
1218
+ agentPromptEntity = await md.GetEntityObject<any>(
1219
+ 'MJ: AI Agent Prompts',
1220
+ this._contextUser
1221
+ );
1222
+ agentPromptEntity.AgentID = agentId;
1223
+ agentPromptEntity.PromptID = promptEntity.ID;
1224
+ console.log(`💬 savePrompts: Creating new junction for prompt ${i + 1}`);
1225
+ }
1226
+
1227
+ // Set/update common fields (ExecutionOrder might change)
861
1228
  agentPromptEntity.ExecutionOrder = i;
862
1229
  agentPromptEntity.Status = 'Active';
863
1230
 
@@ -866,12 +1233,236 @@ export class AgentSpecSync {
866
1233
  throw new Error(`Failed to save agent-prompt junction for prompt ${i + 1}`);
867
1234
  }
868
1235
 
869
- console.log(`✅ savePrompts: Created AIAgentPrompt junction with ID: ${agentPromptEntity.ID}`);
1236
+ // Track mutation
1237
+ this.trackMutation(
1238
+ 'AI Agent Prompts',
1239
+ isJunctionUpdate ? 'Update' : 'Create',
1240
+ agentPromptEntity.ID,
1241
+ `${isJunctionUpdate ? 'Updated' : 'Created'} agent-prompt junction for prompt ${promptEntity.ID}`
1242
+ );
1243
+
1244
+ console.log(`✅ savePrompts: ${isJunctionUpdate ? 'Updated' : 'Created'} AIAgentPrompt junction with ID: ${agentPromptEntity.ID}`);
870
1245
  }
871
1246
 
872
1247
  console.log(`✅ savePrompts: Successfully saved all ${this.spec.Prompts.length} prompt(s)`);
873
1248
  }
874
1249
 
1250
+ /**
1251
+ * Save all steps for this agent (Flow agents only).
1252
+ *
1253
+ * Creates AIAgentStep records for each step in the spec. Steps define the nodes
1254
+ * in a Flow agent's execution graph.
1255
+ *
1256
+ * @private
1257
+ * @param agentId - The parent agent ID
1258
+ * @throws {Error} If any step save fails
1259
+ */
1260
+ private async saveSteps(agentId: string): Promise<void> {
1261
+ console.log(`🔷 saveSteps: Called with agentId=${agentId}, Steps=${this.spec.Steps ? this.spec.Steps.length : 'undefined'}`);
1262
+
1263
+ if (!this.spec.Steps || this.spec.Steps.length === 0) {
1264
+ console.log('🔷 saveSteps: No steps to save, returning early');
1265
+ return;
1266
+ }
1267
+
1268
+ console.log(`🔷 saveSteps: Processing ${this.spec.Steps.length} step(s)...`);
1269
+ const md = new Metadata();
1270
+
1271
+ for (const stepSpec of this.spec.Steps) {
1272
+ const stepEntity = await md.GetEntityObject<AIAgentStepEntity>(
1273
+ 'MJ: AI Agent Steps',
1274
+ this._contextUser
1275
+ );
1276
+
1277
+ // Load existing if ID provided
1278
+ const isUpdate = !!(stepSpec.ID && stepSpec.ID !== '');
1279
+ if (isUpdate) {
1280
+ await stepEntity.Load(stepSpec.ID);
1281
+ }
1282
+
1283
+ // Map fields
1284
+ stepEntity.AgentID = agentId;
1285
+ stepEntity.Name = stepSpec.Name;
1286
+ stepEntity.Description = stepSpec.Description || null;
1287
+ stepEntity.StepType = stepSpec.StepType;
1288
+ stepEntity.StartingStep = stepSpec.StartingStep;
1289
+ stepEntity.ActionID = stepSpec.ActionID || null;
1290
+ stepEntity.SubAgentID = stepSpec.SubAgentID || null;
1291
+ stepEntity.Status = 'Active'; // Default to Active
1292
+
1293
+ // Handle inline prompt creation for Prompt-type steps
1294
+ // If StepType is Prompt and PromptID is empty, create a new AIPrompt record
1295
+ if (stepSpec.StepType === 'Prompt' && (!stepSpec.PromptID || stepSpec.PromptID === '')) {
1296
+ if (stepSpec.PromptText) {
1297
+ console.log(`🔷 saveSteps: Creating inline prompt for step "${stepSpec.Name}"`);
1298
+
1299
+ // Create AIPrompt entity (using any type for TemplateText dynamic property)
1300
+ const promptEntity = await md.GetEntityObject<any>(
1301
+ 'AI Prompts',
1302
+ this._contextUser
1303
+ );
1304
+
1305
+ // Set prompt fields
1306
+ promptEntity.Name = stepSpec.PromptName || `${stepSpec.Name} Prompt`;
1307
+ promptEntity.Description = stepSpec.PromptDescription || `Prompt for step: ${stepSpec.Name}`;
1308
+ promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
1309
+ promptEntity.Status = 'Active';
1310
+ promptEntity.ResponseFormat = 'JSON';
1311
+ promptEntity.TemplateText = stepSpec.PromptText; // TemplateText is a dynamic property
1312
+
1313
+ const promptSaved = await promptEntity.Save();
1314
+ if (!promptSaved) {
1315
+ throw new Error(`Failed to save inline prompt for step "${stepSpec.Name}"`);
1316
+ }
1317
+
1318
+ // Track mutation
1319
+ this.trackMutation(
1320
+ 'AI Prompts',
1321
+ 'Create',
1322
+ promptEntity.ID,
1323
+ `Created inline prompt: ${promptEntity.Name}`
1324
+ );
1325
+
1326
+ console.log(`✅ saveSteps: Created inline AIPrompt with ID: ${promptEntity.ID}`);
1327
+
1328
+ // Link the created prompt to this step
1329
+ stepEntity.PromptID = promptEntity.ID;
1330
+ stepSpec.PromptID = promptEntity.ID; // Update spec too
1331
+ } else {
1332
+ console.warn(`⚠️ saveSteps: Step "${stepSpec.Name}" is Prompt type with empty PromptID but no PromptText provided`);
1333
+ stepEntity.PromptID = null;
1334
+ }
1335
+ } else {
1336
+ stepEntity.PromptID = stepSpec.PromptID || null;
1337
+ }
1338
+
1339
+ // Map Action I/O mappings for Flow agent action steps
1340
+ // Handle ActionInputMapping - convert object to JSON string if needed
1341
+ if (stepSpec.ActionInputMapping) {
1342
+ stepEntity.ActionInputMapping = typeof stepSpec.ActionInputMapping === 'string'
1343
+ ? stepSpec.ActionInputMapping
1344
+ : JSON.stringify(stepSpec.ActionInputMapping);
1345
+ } else {
1346
+ stepEntity.ActionInputMapping = null;
1347
+ }
1348
+
1349
+ // Handle ActionOutputMapping - convert object to JSON string if needed
1350
+ if (stepSpec.ActionOutputMapping) {
1351
+ stepEntity.ActionOutputMapping = typeof stepSpec.ActionOutputMapping === 'string'
1352
+ ? stepSpec.ActionOutputMapping
1353
+ : JSON.stringify(stepSpec.ActionOutputMapping);
1354
+ } else {
1355
+ stepEntity.ActionOutputMapping = null;
1356
+ }
1357
+
1358
+ const saved = await stepEntity.Save();
1359
+ if (!saved) {
1360
+ throw new Error(`Failed to save step "${stepSpec.Name}" for agent ${this.spec.Name}`);
1361
+ }
1362
+
1363
+ // Track mutation
1364
+ this.trackMutation(
1365
+ 'AI Agent Steps',
1366
+ isUpdate ? 'Update' : 'Create',
1367
+ stepEntity.ID,
1368
+ `${isUpdate ? 'Updated' : 'Created'} step: ${stepSpec.Name}`
1369
+ );
1370
+
1371
+ // Update the spec with the saved ID so paths can reference it
1372
+ stepSpec.ID = stepEntity.ID;
1373
+
1374
+ console.log(`✅ saveSteps: Created AIAgentStep with ID: ${stepEntity.ID}`);
1375
+ }
1376
+
1377
+ console.log(`✅ saveSteps: Successfully saved all ${this.spec.Steps.length} step(s)`);
1378
+ }
1379
+
1380
+ /**
1381
+ * Save all step paths for this agent (Flow agents only).
1382
+ *
1383
+ * Creates AIAgentStepPath records that define the edges in a Flow agent's
1384
+ * execution graph, including conditional branching logic.
1385
+ *
1386
+ * @private
1387
+ * @param agentId - The parent agent ID
1388
+ * @throws {Error} If any path save fails
1389
+ */
1390
+ private async saveStepPaths(agentId: string): Promise<void> {
1391
+ console.log(`🔶 saveStepPaths: Called with agentId=${agentId}, Paths=${this.spec.Paths ? this.spec.Paths.length : 'undefined'}`);
1392
+
1393
+ if (!this.spec.Paths || this.spec.Paths.length === 0) {
1394
+ console.log('🔶 saveStepPaths: No paths to save, returning early');
1395
+ return;
1396
+ }
1397
+
1398
+ console.log(`🔶 saveStepPaths: Processing ${this.spec.Paths.length} path(s)...`);
1399
+ const md = new Metadata();
1400
+
1401
+ // Create a map of step names to IDs for easy lookup
1402
+ const stepNameToId = new Map<string, string>();
1403
+ if (this.spec.Steps) {
1404
+ for (const step of this.spec.Steps) {
1405
+ if (step.ID) {
1406
+ stepNameToId.set(step.Name, step.ID);
1407
+ }
1408
+ }
1409
+ }
1410
+
1411
+ for (const pathSpec of this.spec.Paths) {
1412
+ const pathEntity = await md.GetEntityObject<AIAgentStepPathEntity>(
1413
+ 'MJ: AI Agent Step Paths',
1414
+ this._contextUser
1415
+ );
1416
+
1417
+ // Load existing if ID provided
1418
+ const isUpdate = !!(pathSpec.ID && pathSpec.ID !== '');
1419
+ if (isUpdate) {
1420
+ await pathEntity.Load(pathSpec.ID);
1421
+ }
1422
+
1423
+ // Resolve step IDs from step names
1424
+ // OriginStepID and DestinationStepID can be either step IDs or step names
1425
+ // If they're names, look them up in our map
1426
+ let originStepId = pathSpec.OriginStepID;
1427
+ let destinationStepId = pathSpec.DestinationStepID;
1428
+
1429
+ // Try to resolve as step names first
1430
+ if (stepNameToId.has(pathSpec.OriginStepID)) {
1431
+ originStepId = stepNameToId.get(pathSpec.OriginStepID)!;
1432
+ }
1433
+ if (stepNameToId.has(pathSpec.DestinationStepID)) {
1434
+ destinationStepId = stepNameToId.get(pathSpec.DestinationStepID)!;
1435
+ }
1436
+
1437
+ // Map fields
1438
+ pathEntity.OriginStepID = originStepId;
1439
+ pathEntity.DestinationStepID = destinationStepId;
1440
+ pathEntity.Condition = pathSpec.Condition || null;
1441
+ pathEntity.Description = pathSpec.Description || null;
1442
+ pathEntity.Priority = pathSpec.Priority;
1443
+
1444
+ const saved = await pathEntity.Save();
1445
+ if (!saved) {
1446
+ throw new Error(`Failed to save path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`);
1447
+ }
1448
+
1449
+ // Track mutation
1450
+ this.trackMutation(
1451
+ 'AI Agent Step Paths',
1452
+ isUpdate ? 'Update' : 'Create',
1453
+ pathEntity.ID,
1454
+ `${isUpdate ? 'Updated' : 'Created'} path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`
1455
+ );
1456
+
1457
+ // Update the spec with the saved ID
1458
+ pathSpec.ID = pathEntity.ID;
1459
+
1460
+ console.log(`✅ saveStepPaths: Created AIAgentStepPath with ID: ${pathEntity.ID}`);
1461
+ }
1462
+
1463
+ console.log(`✅ saveStepPaths: Successfully saved all ${this.spec.Paths.length} path(s)`);
1464
+ }
1465
+
875
1466
  // ===== UTILITY METHODS =====
876
1467
 
877
1468
  /**
@@ -909,6 +1500,11 @@ export class AgentSpecSync {
909
1500
  ID: partial.ID || '',
910
1501
  Name: partial.Name || '',
911
1502
  Description: partial.Description,
1503
+
1504
+ // TypeID and Status are extended fields (not in base AgentSpec interface)
1505
+ TypeID: (partial as any).TypeID,
1506
+ Status: (partial as any).Status || 'Active',
1507
+
912
1508
  IconClass: partial.IconClass,
913
1509
  LogoURL: partial.LogoURL,
914
1510
  ParentID: partial.ParentID,
@@ -935,9 +1531,15 @@ export class AgentSpecSync {
935
1531
  DefaultArtifactTypeID: partial.DefaultArtifactTypeID,
936
1532
  OwnerUserID: partial.OwnerUserID,
937
1533
  InvocationMode: partial.InvocationMode || 'Any',
1534
+ FunctionalRequirements: partial.FunctionalRequirements,
1535
+ TechnicalDesign: partial.TechnicalDesign,
938
1536
  Actions: partial.Actions || [],
939
1537
  SubAgents: partial.SubAgents || [],
940
- Prompts: partial.Prompts || []
1538
+ Prompts: partial.Prompts || [],
1539
+
1540
+ // Flow agent fields - critical for Flow agent support
1541
+ Steps: partial.Steps || [],
1542
+ Paths: partial.Paths || []
941
1543
  };
942
1544
  }
943
1545