@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.
@@ -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;
@@ -91,6 +96,32 @@ class AgentSpecSync {
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
  }
@@ -367,6 +497,71 @@ class AgentSpecSync {
367
497
  SubAgentContextPaths: this.parseJsonField(relationship.SubAgentContextPaths)
368
498
  };
369
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
+ }
370
565
  // ===== SAVING METHODS =====
371
566
  /**
372
567
  * Save the current spec to the database.
@@ -378,7 +573,7 @@ class AgentSpecSync {
378
573
  * database schema are executed, and any failures will prevent the save.
379
574
  *
380
575
  * @param validate - Whether to validate before saving (default: true)
381
- * @returns Promise resolving to the saved agent ID
576
+ * @returns Promise resolving to result with agent ID, success flag, and all mutations performed
382
577
  * @throws {Error} If validation fails or save operation fails
383
578
  *
384
579
  * @example
@@ -388,26 +583,51 @@ class AgentSpecSync {
388
583
  * InvocationMode: 'Any'
389
584
  * }, contextUser);
390
585
  *
391
- * const agentId = await spec.SaveToDatabase();
392
- * 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);
393
589
  * ```
394
590
  */
395
591
  async SaveToDatabase(validate = true) {
592
+ // Clear previous mutations
593
+ this.clearMutations();
396
594
  if (!this._isDirty && this._isLoaded) {
397
595
  // No changes to save
398
- return this.spec.ID;
399
- }
400
- // Step 1: Save main agent entity
401
- const agentId = await this.saveAgentEntity(validate);
402
- // Step 2: Save actions
403
- await this.saveActions(agentId);
404
- // Step 3: Save sub-agents (both child and related)
405
- await this.saveSubAgents(agentId);
406
- // Step 4: Save prompts
407
- await this.savePrompts(agentId);
408
- this._isDirty = false;
409
- this._isLoaded = true;
410
- 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
+ }
411
631
  }
412
632
  /**
413
633
  * Save the main AIAgent entity.
@@ -420,6 +640,8 @@ class AgentSpecSync {
420
640
  async saveAgentEntity(validate) {
421
641
  const md = new core_1.Metadata();
422
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;
423
645
  // If ID exists, load existing record
424
646
  if (this.spec.ID) {
425
647
  const loaded = await agentEntity.Load(this.spec.ID);
@@ -435,6 +657,17 @@ class AgentSpecSync {
435
657
  agentEntity.ParentID = this.spec.ParentID || null;
436
658
  agentEntity.DriverClass = this.spec.DriverClass || null;
437
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
+ }
438
671
  // Serialize JSON fields
439
672
  agentEntity.PayloadDownstreamPaths = JSON.stringify(this.spec.PayloadDownstreamPaths || ['*']);
440
673
  agentEntity.PayloadUpstreamPaths = JSON.stringify(this.spec.PayloadUpstreamPaths || ['*']);
@@ -467,6 +700,9 @@ class AgentSpecSync {
467
700
  agentEntity.OwnerUserID = this.spec.OwnerUserID;
468
701
  }
469
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;
470
706
  // Validate if requested
471
707
  if (validate) {
472
708
  const validation = agentEntity.Validate();
@@ -482,6 +718,8 @@ class AgentSpecSync {
482
718
  }
483
719
  // Update spec with saved ID
484
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}`);
485
723
  return agentEntity.ID;
486
724
  }
487
725
  /**
@@ -502,6 +740,8 @@ class AgentSpecSync {
502
740
  const md = new core_1.Metadata();
503
741
  for (const actionSpec of this.spec.Actions) {
504
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;
505
745
  // Load existing if ID provided
506
746
  if (actionSpec.AgentActionID) {
507
747
  await actionEntity.Load(actionSpec.AgentActionID);
@@ -522,6 +762,8 @@ class AgentSpecSync {
522
762
  }
523
763
  // Update spec with saved ID
524
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}`);
525
767
  }
526
768
  }
527
769
  /**
@@ -576,32 +818,25 @@ class AgentSpecSync {
576
818
  // Recursively create the sub-agent using AgentSpecSync
577
819
  const childSync = new AgentSpecSync(childSpec, this._contextUser);
578
820
  childSync.markDirty();
579
- const childId = await childSync.SaveToDatabase();
821
+ const childResult = await childSync.SaveToDatabase();
580
822
  // Update the SubAgentSpec with the created ID so parent can reference it
581
- SubAgentSpec.SubAgent.ID = childId;
582
- console.log(`✅ saveChildSubAgent: Created child sub-agent with ID: ${childId}`);
823
+ SubAgentSpec.SubAgent.ID = childResult.agentId;
824
+ console.log(`✅ saveChildSubAgent: Created child sub-agent with ID: ${childResult.agentId}`);
583
825
  }
584
826
  else {
585
- // SubAgent already exists - just ensure ParentID is set correctly
827
+ // SubAgent already exists - update it recursively to capture all field changes
586
828
  console.log(`🔗 saveChildSubAgent: Updating existing child sub-agent "${SubAgentSpec.SubAgent.ID}"...`);
587
- const md = new core_1.Metadata();
588
- const childEntity = await md.GetEntityObject('AI Agents', this._contextUser);
589
- const loaded = await childEntity.Load(SubAgentSpec.SubAgent.ID);
590
- if (!loaded) {
591
- throw new Error(`Child agent ${SubAgentSpec.SubAgent.ID} not found in database`);
592
- }
593
- // Update parent ID if needed
594
- if (childEntity.ParentID !== parentId) {
595
- console.log(`🔗 saveChildSubAgent: Setting ParentID to ${parentId}`);
596
- childEntity.ParentID = parentId;
597
- const saved = await childEntity.Save();
598
- if (!saved) {
599
- throw new Error(`Failed to update ParentID for child agent ${SubAgentSpec.SubAgent.ID}`);
600
- }
601
- }
602
- else {
603
- console.log(`✅ saveChildSubAgent: ParentID already correct`);
604
- }
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`);
605
840
  }
606
841
  }
607
842
  /**
@@ -619,7 +854,8 @@ class AgentSpecSync {
619
854
  const md = new core_1.Metadata();
620
855
  const relationshipEntity = await md.GetEntityObject('MJ: AI Agent Relationships', this._contextUser);
621
856
  // Load existing if ID provided
622
- if (SubAgentSpec.AgentRelationshipID) {
857
+ const isUpdate = !!SubAgentSpec.AgentRelationshipID;
858
+ if (isUpdate && SubAgentSpec.AgentRelationshipID) {
623
859
  await relationshipEntity.Load(SubAgentSpec.AgentRelationshipID);
624
860
  }
625
861
  // Map fields
@@ -649,6 +885,8 @@ class AgentSpecSync {
649
885
  if (!saved) {
650
886
  throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`);
651
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}`);
652
890
  // Update spec with saved ID
653
891
  SubAgentSpec.AgentRelationshipID = relationshipEntity.ID;
654
892
  }
@@ -671,11 +909,25 @@ class AgentSpecSync {
671
909
  }
672
910
  console.log(`💬 savePrompts: Processing ${this.spec.Prompts.length} prompt(s)...`);
673
911
  const md = new core_1.Metadata();
912
+ const rv = new core_1.RunView();
913
+ // Step 1: Create or update AIPrompt records
674
914
  for (let i = 0; i < this.spec.Prompts.length; i++) {
675
915
  const promptSpec = this.spec.Prompts[i];
676
- // Create AIPrompt entity
677
- const promptEntity = await md.GetEntityObject('AI Prompts', this._contextUser);
678
- // Set required fields
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)
679
931
  promptEntity.Name = `${this.spec.Name} - Prompt ${i + 1}`;
680
932
  promptEntity.Description = `Agent prompt ${i + 1} for ${this.spec.Name}`;
681
933
  promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
@@ -705,21 +957,208 @@ class AgentSpecSync {
705
957
  if (!saved) {
706
958
  throw new Error(`Failed to save prompt ${i + 1} for agent ${this.spec.Name}`);
707
959
  }
708
- console.log(`✅ savePrompts: Created AIPrompt with ID: ${promptEntity.ID}`);
709
- // Create AIAgentPrompt junction
710
- const agentPromptEntity = await md.GetEntityObject('MJ: AI Agent Prompts', this._contextUser);
711
- agentPromptEntity.AgentID = agentId;
712
- agentPromptEntity.PromptID = promptEntity.ID;
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)
713
988
  agentPromptEntity.ExecutionOrder = i;
714
989
  agentPromptEntity.Status = 'Active';
715
990
  const junctionSaved = await agentPromptEntity.Save();
716
991
  if (!junctionSaved) {
717
992
  throw new Error(`Failed to save agent-prompt junction for prompt ${i + 1}`);
718
993
  }
719
- console.log(`✅ savePrompts: Created AIAgentPrompt junction with ID: ${agentPromptEntity.ID}`);
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}`);
720
997
  }
721
998
  console.log(`✅ savePrompts: Successfully saved all ${this.spec.Prompts.length} prompt(s)`);
722
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
+ }
723
1162
  // ===== UTILITY METHODS =====
724
1163
  /**
725
1164
  * Parse a JSON string field, returning undefined if null/empty.
@@ -757,6 +1196,9 @@ class AgentSpecSync {
757
1196
  ID: partial.ID || '',
758
1197
  Name: partial.Name || '',
759
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',
760
1202
  IconClass: partial.IconClass,
761
1203
  LogoURL: partial.LogoURL,
762
1204
  ParentID: partial.ParentID,
@@ -783,9 +1225,14 @@ class AgentSpecSync {
783
1225
  DefaultArtifactTypeID: partial.DefaultArtifactTypeID,
784
1226
  OwnerUserID: partial.OwnerUserID,
785
1227
  InvocationMode: partial.InvocationMode || 'Any',
1228
+ FunctionalRequirements: partial.FunctionalRequirements,
1229
+ TechnicalDesign: partial.TechnicalDesign,
786
1230
  Actions: partial.Actions || [],
787
1231
  SubAgents: partial.SubAgents || [],
788
- Prompts: partial.Prompts || []
1232
+ Prompts: partial.Prompts || [],
1233
+ // Flow agent fields - critical for Flow agent support
1234
+ Steps: partial.Steps || [],
1235
+ Paths: partial.Paths || []
789
1236
  };
790
1237
  }
791
1238
  /**