@memberjunction/ai-agent-manager 2.108.0 → 2.109.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.
@@ -109,7 +109,7 @@ export class AgentSpecSync {
109
109
  this.spec = {
110
110
  ID: '', // Will be set on save if empty
111
111
  Name: '',
112
- StartingPayloadValidationMode: () => 'Fail',
112
+ StartingPayloadValidationMode: 'Fail',
113
113
  Actions: [],
114
114
  SubAgents: []
115
115
  };
@@ -413,7 +413,11 @@ export class AgentSpecSync {
413
413
  private mapChildAgentToSpec(childAgent: AIAgentEntity): SubAgentSpec {
414
414
  return {
415
415
  Type: 'child',
416
- SubAgentID: childAgent.ID
416
+ SubAgent: {
417
+ ID: childAgent.ID,
418
+ Name: childAgent.Name || '',
419
+ StartingPayloadValidationMode: 'Fail'
420
+ }
417
421
  };
418
422
  }
419
423
 
@@ -427,7 +431,11 @@ export class AgentSpecSync {
427
431
  private mapRelatedAgentToSpec(relationship: AIAgentRelationshipEntity): SubAgentSpec {
428
432
  return {
429
433
  Type: 'related',
430
- SubAgentID: relationship.SubAgentID,
434
+ SubAgent: {
435
+ ID: relationship.SubAgentID,
436
+ Name: relationship.SubAgent || '',
437
+ StartingPayloadValidationMode: 'Fail'
438
+ },
431
439
  AgentRelationshipID: relationship.ID,
432
440
  SubAgentInputMapping: this.parseJsonField<Record<string, string>>(relationship.SubAgentInputMapping),
433
441
  SubAgentOutputMapping: this.parseJsonField<Record<string, string>>(relationship.SubAgentOutputMapping),
@@ -476,6 +484,9 @@ export class AgentSpecSync {
476
484
  // Step 3: Save sub-agents (both child and related)
477
485
  await this.saveSubAgents(agentId);
478
486
 
487
+ // Step 4: Save prompts
488
+ await this.savePrompts(agentId);
489
+
479
490
  this._isDirty = false;
480
491
  this._isLoaded = true;
481
492
 
@@ -535,7 +546,7 @@ export class AgentSpecSync {
535
546
  agentEntity.FinalPayloadValidationMaxRetries = this.spec.FinalPayloadValidationMaxRetries || 3;
536
547
 
537
548
  agentEntity.StartingPayloadValidation = this.spec.StartingPayloadValidation || null;
538
- agentEntity.StartingPayloadValidationMode = this.spec.StartingPayloadValidationMode?.() || 'Fail';
549
+ agentEntity.StartingPayloadValidationMode = this.spec.StartingPayloadValidationMode || 'Fail';
539
550
 
540
551
  // Resource limits
541
552
  agentEntity.MaxCostPerRun = this.spec.MaxCostPerRun || null;
@@ -655,38 +666,64 @@ export class AgentSpecSync {
655
666
  * Save a child sub-agent (ParentID-based relationship).
656
667
  *
657
668
  * For child agents, the relationship is established by setting the ParentID field
658
- * on the child agent entity. If the SubAgentID is empty, this indicates a new
659
- * child agent that needs to be created.
669
+ * on the child agent entity. If the SubAgent.ID is empty, this creates a new
670
+ * child agent recursively using AgentSpecSync.
671
+ *
672
+ * This enables creating complete agent hierarchies in one call - sub-agents are
673
+ * created first (depth-first), then parent references them via ParentID.
660
674
  *
661
675
  * @private
662
676
  * @param parentId - The parent agent ID
663
677
  * @param SubAgentSpec - The sub-agent specification
664
- * @throws {Error} If child agent doesn't exist or save fails
678
+ * @throws {Error} If child agent save fails
665
679
  */
666
680
  private async saveChildSubAgent(parentId: string, SubAgentSpec: SubAgentSpec): Promise<void> {
667
- if (!SubAgentSpec.SubAgentID) {
668
- throw new Error('Child sub-agent must have a SubAgentID');
669
- }
681
+ console.log(`🔗 saveChildSubAgent: Processing child "${SubAgentSpec.SubAgent?.Name}", ID="${SubAgentSpec.SubAgent?.ID}"`);
670
682
 
671
- // For child agents, we just need to ensure the ParentID is set correctly
672
- // The sub-agent itself should already exist or be created separately
673
- const md = new Metadata();
674
- const childEntity = await md.GetEntityObject<AIAgentEntity>(
675
- 'AI Agents',
676
- this._contextUser
677
- );
683
+ // If SubAgent.ID is empty or missing, create the sub-agent recursively
684
+ if (!SubAgentSpec.SubAgent?.ID || SubAgentSpec.SubAgent.ID === '') {
685
+ console.log(`🔨 saveChildSubAgent: Creating new child sub-agent "${SubAgentSpec.SubAgent.Name}"...`);
678
686
 
679
- const loaded = await childEntity.Load(SubAgentSpec.SubAgentID);
680
- if (!loaded) {
681
- throw new Error(`Child agent ${SubAgentSpec.SubAgentID} not found`);
682
- }
687
+ // Set ParentID in the sub-agent spec before creating it
688
+ const childSpec: AgentSpec = {
689
+ ...SubAgentSpec.SubAgent,
690
+ ParentID: parentId
691
+ };
683
692
 
684
- // Update parent ID if needed
685
- if (childEntity.ParentID !== parentId) {
686
- childEntity.ParentID = parentId;
687
- const saved = await childEntity.Save();
688
- if (!saved) {
689
- throw new Error(`Failed to update ParentID for child agent ${SubAgentSpec.SubAgentID}`);
693
+ // Recursively create the sub-agent using AgentSpecSync
694
+ const childSync = new AgentSpecSync(childSpec, this._contextUser);
695
+ childSync.markDirty();
696
+ const childId = await childSync.SaveToDatabase();
697
+
698
+ // Update the SubAgentSpec with the created ID so parent can reference it
699
+ SubAgentSpec.SubAgent.ID = childId;
700
+
701
+ console.log(`✅ saveChildSubAgent: Created child sub-agent with ID: ${childId}`);
702
+ } else {
703
+ // SubAgent already exists - just ensure ParentID is set correctly
704
+ console.log(`🔗 saveChildSubAgent: Updating existing child sub-agent "${SubAgentSpec.SubAgent.ID}"...`);
705
+
706
+ const md = new Metadata();
707
+ const childEntity = await md.GetEntityObject<AIAgentEntity>(
708
+ 'AI Agents',
709
+ this._contextUser
710
+ );
711
+
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
+ }
716
+
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`);
690
727
  }
691
728
  }
692
729
  }
@@ -716,7 +753,7 @@ export class AgentSpecSync {
716
753
 
717
754
  // Map fields
718
755
  relationshipEntity.AgentID = agentId;
719
- relationshipEntity.SubAgentID = SubAgentSpec.SubAgentID;
756
+ relationshipEntity.SubAgentID = SubAgentSpec.SubAgent.ID;
720
757
  relationshipEntity.Status = 'Active';
721
758
 
722
759
  // Serialize mapping fields
@@ -740,13 +777,101 @@ export class AgentSpecSync {
740
777
 
741
778
  const saved = await relationshipEntity.Save();
742
779
  if (!saved) {
743
- throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgentID}`);
780
+ throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`);
744
781
  }
745
782
 
746
783
  // Update spec with saved ID
747
784
  SubAgentSpec.AgentRelationshipID = relationshipEntity.ID;
748
785
  }
749
786
 
787
+ /**
788
+ * Save all prompts for this agent.
789
+ *
790
+ * Creates AIPrompt records (with template) and AIAgentPrompt junction records.
791
+ * Supports simplified prompt format from Architect Agent with just PromptText,
792
+ * PromptRole, and PromptPosition.
793
+ *
794
+ * @private
795
+ * @param agentId - The parent agent ID
796
+ * @throws {Error} If any prompt save fails
797
+ */
798
+ private async savePrompts(agentId: string): Promise<void> {
799
+ console.log(`💬 savePrompts: Called with agentId=${agentId}, Prompts=${this.spec.Prompts ? this.spec.Prompts.length : 'undefined'}`);
800
+
801
+ if (!this.spec.Prompts || this.spec.Prompts.length === 0) {
802
+ console.log('💬 savePrompts: No prompts to save, returning early');
803
+ return;
804
+ }
805
+
806
+ console.log(`💬 savePrompts: Processing ${this.spec.Prompts.length} prompt(s)...`);
807
+ const md = new Metadata();
808
+
809
+ for (let i = 0; i < this.spec.Prompts.length; i++) {
810
+ const promptSpec = this.spec.Prompts[i];
811
+
812
+ // Create AIPrompt entity
813
+ const promptEntity = await md.GetEntityObject<any>(
814
+ 'AI Prompts',
815
+ this._contextUser
816
+ );
817
+
818
+ // Set required fields
819
+ promptEntity.Name = `${this.spec.Name} - Prompt ${i + 1}`;
820
+ promptEntity.Description = `Agent prompt ${i + 1} for ${this.spec.Name}`;
821
+ promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
822
+ promptEntity.Status = 'Active';
823
+ promptEntity.ResponseFormat = 'JSON';
824
+
825
+ // Handle prompt text - supports both string and object formats
826
+ if (typeof (promptSpec as any).PromptText === 'string') {
827
+ promptEntity.TemplateText = (promptSpec as any).PromptText;
828
+ } else if (typeof (promptSpec as any).PromptText === 'object') {
829
+ // Architect may send PromptText as {text: "...", json: {...}}
830
+ const promptTextObj = (promptSpec as any).PromptText as any;
831
+ let combinedText = promptTextObj.text || '';
832
+ if (promptTextObj.json) {
833
+ combinedText += '\n\n```json\n' + JSON.stringify(promptTextObj.json, null, 2) + '\n```';
834
+ }
835
+ promptEntity.TemplateText = combinedText;
836
+ }
837
+
838
+ // Set prompt role and position if provided
839
+ if ((promptSpec as any).PromptRole) {
840
+ promptEntity.PromptRole = (promptSpec as any).PromptRole;
841
+ }
842
+ if ((promptSpec as any).PromptPosition) {
843
+ promptEntity.PromptPosition = (promptSpec as any).PromptPosition;
844
+ }
845
+
846
+ const saved = await promptEntity.Save();
847
+ if (!saved) {
848
+ throw new Error(`Failed to save prompt ${i + 1} for agent ${this.spec.Name}`);
849
+ }
850
+
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
857
+ );
858
+
859
+ agentPromptEntity.AgentID = agentId;
860
+ agentPromptEntity.PromptID = promptEntity.ID;
861
+ agentPromptEntity.ExecutionOrder = i;
862
+ agentPromptEntity.Status = 'Active';
863
+
864
+ const junctionSaved = await agentPromptEntity.Save();
865
+ if (!junctionSaved) {
866
+ throw new Error(`Failed to save agent-prompt junction for prompt ${i + 1}`);
867
+ }
868
+
869
+ console.log(`✅ savePrompts: Created AIAgentPrompt junction with ID: ${agentPromptEntity.ID}`);
870
+ }
871
+
872
+ console.log(`✅ savePrompts: Successfully saved all ${this.spec.Prompts.length} prompt(s)`);
873
+ }
874
+
750
875
  // ===== UTILITY METHODS =====
751
876
 
752
877
  /**
@@ -804,14 +929,15 @@ export class AgentSpecSync {
804
929
  MinExecutionsPerRun: partial.MinExecutionsPerRun,
805
930
  MaxExecutionsPerRun: partial.MaxExecutionsPerRun,
806
931
  StartingPayloadValidation: partial.StartingPayloadValidation,
807
- StartingPayloadValidationMode: partial.StartingPayloadValidationMode || (() => 'Fail'),
932
+ StartingPayloadValidationMode: partial.StartingPayloadValidationMode || 'Fail',
808
933
  DefaultPromptEffortLevel: partial.DefaultPromptEffortLevel,
809
934
  ChatHandlingOption: partial.ChatHandlingOption,
810
935
  DefaultArtifactTypeID: partial.DefaultArtifactTypeID,
811
936
  OwnerUserID: partial.OwnerUserID,
812
937
  InvocationMode: partial.InvocationMode || 'Any',
813
938
  Actions: partial.Actions || [],
814
- SubAgents: partial.SubAgents || []
939
+ SubAgents: partial.SubAgents || [],
940
+ Prompts: partial.Prompts || []
815
941
  };
816
942
  }
817
943
 
@@ -871,4 +997,4 @@ export class AgentSpecSync {
871
997
  public markDirty(): void {
872
998
  this._isDirty = true;
873
999
  }
874
- }
1000
+ }
@@ -0,0 +1,293 @@
1
+ import { BaseAgent, PayloadManager } from '@memberjunction/ai-agents';
2
+ import { ExecuteAgentParams, BaseAgentNextStep, AgentSpec } from '@memberjunction/ai-core-plus';
3
+ import { AIAgentRunEntityExtended, AIAgentRunStepEntityExtended, ActionEntity, AIPromptEntity } from '@memberjunction/core-entities';
4
+ import { Metadata, RunView } from '@memberjunction/core';
5
+ import { RegisterClass } from '@memberjunction/global';
6
+
7
+ /**
8
+ * Architect Agent - Transforms technical design into validated AgentSpec JSON
9
+ *
10
+ * This agent creates AgentSpec objects from technical designs and validates them
11
+ * before passing to the Builder Agent for persistence. It overrides validateSuccessNextStep
12
+ * to ensure the generated AgentSpec is valid before proceeding.
13
+ *
14
+ * Key responsibilities:
15
+ * - Transform design documents into AgentSpec format
16
+ * - Validate required fields are present
17
+ * - Verify action IDs exist in database
18
+ * - Verify prompt IDs exist (if specified)
19
+ * - Validate SubAgent structure
20
+ * - Auto-correct minor issues (casing, missing optional fields)
21
+ * - Force retry with detailed errors if validation fails
22
+ */
23
+ @RegisterClass(BaseAgent, 'AgentArchitectAgent')
24
+ export class AgentArchitectAgent extends BaseAgent {
25
+
26
+ /**
27
+ * Applies payload changes if any are requested in the next step
28
+ */
29
+ protected applyPayloadChanges<P>(payload: P, nextStep: BaseAgentNextStep<P>): P {
30
+ if (nextStep.payloadChangeRequest) {
31
+ const pm = new PayloadManager();
32
+ const result = pm.applyAgentChangeRequest<P>(payload, nextStep.payloadChangeRequest);
33
+ return result.result || payload;
34
+ }
35
+ return payload;
36
+ }
37
+
38
+ protected override async validateSuccessNextStep<P>(
39
+ params: ExecuteAgentParams,
40
+ nextStep: BaseAgentNextStep<P>,
41
+ currentPayload: P,
42
+ agentRun: AIAgentRunEntityExtended,
43
+ currentStep: AIAgentRunStepEntityExtended
44
+ ): Promise<BaseAgentNextStep<P>> {
45
+ // First call base validation
46
+ const baseValidation = await super.validateSuccessNextStep(params, nextStep, currentPayload, agentRun, currentStep);
47
+ if (baseValidation.step === 'Retry') {
48
+ return baseValidation;
49
+ }
50
+
51
+ // Apply payload changes to get the spec that would be sent forward
52
+ const payloadWithChanges = this.applyPayloadChanges<P>(currentPayload, nextStep);
53
+
54
+ // Extract the agentSpec from the nested payload structure
55
+ // The payload has structure: { metadata, requirements, design, agentSpec }
56
+ const agentSpec = (payloadWithChanges as any).agentSpec as AgentSpec;
57
+
58
+ console.log('🏗️ Architect Agent: Validating AgentSpec:', agentSpec ? `Name="${agentSpec.Name}"` : 'agentSpec is null/undefined');
59
+
60
+ // Validate the AgentSpec
61
+ const validation = await this.validateAgentSpec(agentSpec, params);
62
+
63
+ if (validation.errors.length > 0) {
64
+ console.log('❌ Architect Agent: Validation failed with errors:', validation.errors);
65
+ // Return retry with detailed error messages
66
+ return {
67
+ ...nextStep,
68
+ step: 'Retry',
69
+ retryInstructions: this.formatValidationErrors(validation.errors),
70
+ newPayload: nextStep.newPayload
71
+ };
72
+ }
73
+
74
+ // If spec was auto-corrected, update the payload with corrected spec
75
+ if (validation.correctedSpec) {
76
+ console.log('✓ Architect Agent: Auto-corrected AgentSpec, updating payload');
77
+ const updatedPayload = {
78
+ ...(payloadWithChanges as any),
79
+ agentSpec: validation.correctedSpec
80
+ };
81
+ return {
82
+ ...nextStep,
83
+ newPayload: updatedPayload as P
84
+ };
85
+ }
86
+
87
+ console.log('✅ Architect Agent: AgentSpec validation passed');
88
+ return nextStep;
89
+ }
90
+
91
+ /**
92
+ * Validates an AgentSpec object
93
+ * Returns errors array and potentially a corrected spec
94
+ */
95
+ protected async validateAgentSpec(
96
+ spec: AgentSpec,
97
+ params: ExecuteAgentParams
98
+ ): Promise<{ errors: string[]; correctedSpec?: AgentSpec }> {
99
+ const errors: string[] = [];
100
+ let corrected = false;
101
+
102
+ // Check if spec exists
103
+ if (!spec) {
104
+ errors.push('❌ AgentSpec is null or undefined. Ensure you place the AgentSpec in payload.agentSpec (inside a payloadChangeRequest with newElements or replaceElements)');
105
+ return { errors };
106
+ }
107
+
108
+ const correctedSpec = { ...spec };
109
+
110
+ // 1. Validate required fields
111
+ if (!correctedSpec.Name || correctedSpec.Name.trim().length === 0) {
112
+ errors.push('❌ AgentSpec.Name is required and cannot be empty');
113
+ }
114
+
115
+ if (!correctedSpec.StartingPayloadValidationMode) {
116
+ // Auto-correct: add default
117
+ correctedSpec.StartingPayloadValidationMode = 'Fail';
118
+ corrected = true;
119
+ console.log('✓ Auto-corrected: Added default StartingPayloadValidationMode = "Fail"');
120
+ } else if (correctedSpec.StartingPayloadValidationMode !== 'Fail' && correctedSpec.StartingPayloadValidationMode !== 'Warn') {
121
+ errors.push(`❌ StartingPayloadValidationMode must be either "Fail" or "Warn", got: "${correctedSpec.StartingPayloadValidationMode}"`);
122
+ }
123
+
124
+ // 2. Validate Actions if specified
125
+ if (correctedSpec.Actions && correctedSpec.Actions.length > 0) {
126
+ const actionValidation = await this.validateActions(correctedSpec.Actions, params);
127
+ if (actionValidation.errors.length > 0) {
128
+ errors.push(...actionValidation.errors);
129
+ }
130
+ }
131
+
132
+ // 3. Validate SubAgents if specified
133
+ if (correctedSpec.SubAgents && correctedSpec.SubAgents.length > 0) {
134
+ const subAgentValidation = this.validateSubAgents(correctedSpec.SubAgents);
135
+ if (subAgentValidation.errors.length > 0) {
136
+ errors.push(...subAgentValidation.errors);
137
+ }
138
+ if (subAgentValidation.corrected) {
139
+ corrected = true;
140
+ }
141
+ }
142
+
143
+ // 4. Validate optional but important fields
144
+ if (correctedSpec.FinalPayloadValidationMode &&
145
+ !['Fail', 'Retry', 'Warn'].includes(correctedSpec.FinalPayloadValidationMode)) {
146
+ errors.push(`❌ FinalPayloadValidationMode must be "Fail", "Retry", or "Warn", got: "${correctedSpec.FinalPayloadValidationMode}"`);
147
+ }
148
+
149
+ if (correctedSpec.InvocationMode &&
150
+ !['Any', 'Agent', 'User', 'Never'].includes(correctedSpec.InvocationMode)) {
151
+ errors.push(`❌ InvocationMode must be "Any", "Agent", "User", or "Never", got: "${correctedSpec.InvocationMode}"`);
152
+ }
153
+
154
+ return {
155
+ errors,
156
+ correctedSpec: corrected ? correctedSpec : undefined
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Validates that action IDs exist in the database
162
+ */
163
+ protected async validateActions(
164
+ actions: AgentSpec['Actions'],
165
+ params: ExecuteAgentParams
166
+ ): Promise<{ errors: string[] }> {
167
+ const errors: string[] = [];
168
+
169
+ if (!actions || actions.length === 0) {
170
+ return { errors };
171
+ }
172
+
173
+ try {
174
+ const rv = new RunView();
175
+ const actionIds = actions
176
+ .map(a => a.ActionID)
177
+ .filter(id => id && id.trim().length > 0);
178
+
179
+ if (actionIds.length === 0) {
180
+ errors.push('❌ All actions must have an ActionID');
181
+ return { errors };
182
+ }
183
+
184
+ // Query database for these action IDs
185
+ const filter = actionIds.map(id => `ID='${id}'`).join(' OR ');
186
+ const result = await rv.RunView<ActionEntity>({
187
+ EntityName: 'Actions',
188
+ ExtraFilter: filter,
189
+ ResultType: 'entity_object'
190
+ }, params.contextUser);
191
+
192
+ if (!result.Success) {
193
+ errors.push(`❌ Failed to validate actions: ${result.ErrorMessage}`);
194
+ return { errors };
195
+ }
196
+
197
+ const foundIds = new Set(result.Results.map(a => a.ID));
198
+
199
+ // Check which actions weren't found
200
+ for (const action of actions) {
201
+ if (!action.ActionID) {
202
+ errors.push('❌ Action is missing ActionID field');
203
+ continue;
204
+ }
205
+
206
+ if (!foundIds.has(action.ActionID)) {
207
+ errors.push(`❌ Action with ID "${action.ActionID}" not found in database. Please use "Find Best Action" or "List Actions" to get valid action IDs.`);
208
+ }
209
+ }
210
+
211
+ } catch (error: any) {
212
+ errors.push(`❌ Error validating actions: ${error?.message || String(error)}`);
213
+ }
214
+
215
+ return { errors };
216
+ }
217
+
218
+ /**
219
+ * Validates SubAgent structure
220
+ */
221
+ protected validateSubAgents(
222
+ subAgents: AgentSpec['SubAgents']
223
+ ): { errors: string[]; corrected: boolean } {
224
+ const errors: string[] = [];
225
+ let corrected = false;
226
+
227
+ if (!subAgents || subAgents.length === 0) {
228
+ return { errors, corrected };
229
+ }
230
+
231
+ for (let i = 0; i < subAgents.length; i++) {
232
+ const subAgent = subAgents[i];
233
+
234
+ // Validate Type field
235
+ if (!subAgent.Type) {
236
+ errors.push(`❌ SubAgent at index ${i} is missing required "Type" field (must be "child" or "related")`);
237
+ continue;
238
+ }
239
+
240
+ if (subAgent.Type !== 'child' && subAgent.Type !== 'related') {
241
+ errors.push(`❌ SubAgent at index ${i} has invalid Type "${subAgent.Type}" (must be "child" or "related")`);
242
+ continue;
243
+ }
244
+
245
+ // Validate SubAgent.SubAgent field
246
+ if (!subAgent.SubAgent) {
247
+ errors.push(`❌ SubAgent at index ${i} is missing required "SubAgent" field`);
248
+ continue;
249
+ }
250
+
251
+ // SubAgent.ID can be empty string "" for NEW sub-agents (Builder creates them recursively)
252
+ // Only validate that ID is not null/undefined
253
+ if (subAgent.SubAgent.ID === null || subAgent.SubAgent.ID === undefined) {
254
+ errors.push(`❌ SubAgent at index ${i} has null/undefined SubAgent.ID (use empty string "" for new sub-agents, or provide existing agent GUID)`);
255
+ }
256
+
257
+ // Validate SubAgent.SubAgent.Name
258
+ if (!subAgent.SubAgent.Name || subAgent.SubAgent.Name.trim().length === 0) {
259
+ errors.push(`❌ SubAgent at index ${i} is missing SubAgent.Name`);
260
+ }
261
+
262
+ // Auto-correct: Add default StartingPayloadValidationMode if missing
263
+ if (!subAgent.SubAgent.StartingPayloadValidationMode) {
264
+ subAgent.SubAgent.StartingPayloadValidationMode = 'Fail';
265
+ corrected = true;
266
+ console.log(`✓ Auto-corrected: Added SubAgent[${i}].SubAgent.StartingPayloadValidationMode = "Fail"`);
267
+ }
268
+
269
+ // Validate related-specific fields
270
+ if (subAgent.Type === 'related' && !subAgent.AgentRelationshipID) {
271
+ // AgentRelationshipID can be empty for new relationships, it will be created on save
272
+ // This is not an error
273
+ }
274
+ }
275
+
276
+ return { errors, corrected };
277
+ }
278
+
279
+ /**
280
+ * Formats validation errors into a clear, actionable message
281
+ */
282
+ protected formatValidationErrors(errors: string[]): string {
283
+ if (errors.length === 0) {
284
+ return '';
285
+ }
286
+
287
+ const header = '## AgentSpec Validation Errors\n\nThe generated AgentSpec has the following issues that must be fixed:\n\n';
288
+ const errorList = errors.map((err, idx) => `${idx + 1}. ${err}`).join('\n');
289
+ const footer = '\n\n**Please fix these issues and regenerate the AgentSpec.**';
290
+
291
+ return header + errorList + footer;
292
+ }
293
+ }
@@ -0,0 +1,89 @@
1
+ import { BaseAgent } from '@memberjunction/ai-agents';
2
+ import { ExecuteAgentParams, AgentConfiguration, BaseAgentNextStep, AgentSpec } from '@memberjunction/ai-core-plus';
3
+ import { RegisterClass } from '@memberjunction/global';
4
+ import { AgentSpecSync } from '../agent-spec-sync';
5
+
6
+ /**
7
+ * Builder Agent - Persists validated AgentSpec to database via AgentSpecSync
8
+ *
9
+ * This is a code-based agent that overrides executeAgentInternal to bypass
10
+ * the normal chat loop and directly execute persistence logic. It receives
11
+ * a validated AgentSpec from the Architect Agent and saves it to the database
12
+ * using AgentSpecSync.
13
+ *
14
+ * Key responsibilities:
15
+ * - Receive validated AgentSpec from payload
16
+ * - Create AgentSpecSync instance
17
+ * - Persist to database
18
+ * - Return success with created agent ID, or failure with error details
19
+ *
20
+ * This agent does NOT iterate - it runs once and returns Success or Failed.
21
+ */
22
+ @RegisterClass(BaseAgent, 'AgentBuilderAgent')
23
+ export class AgentBuilderAgent extends BaseAgent {
24
+
25
+ /**
26
+ * Override executeAgentInternal to run code instead of chat loop
27
+ * This follows the Skip Code Generator pattern
28
+ */
29
+ protected override async executeAgentInternal<P = any>(
30
+ params: ExecuteAgentParams<P>,
31
+ _config: AgentConfiguration
32
+ ): Promise<{ finalStep: BaseAgentNextStep<P>; stepCount: number }> {
33
+
34
+ console.log('🔨 Builder Agent: Starting agent persistence...');
35
+
36
+ try {
37
+ // Extract AgentSpec from nested payload structure
38
+ // The payload has structure: { metadata, requirements, design, agentSpec }
39
+ const agentSpec = (params.payload as any).agentSpec as AgentSpec;
40
+
41
+ if (!agentSpec) {
42
+ throw new Error('No AgentSpec found in payload.agentSpec - ensure Architect Agent placed it there');
43
+ }
44
+
45
+ if (!agentSpec.Name) {
46
+ throw new Error('AgentSpec is missing required Name field');
47
+ }
48
+
49
+ console.log(`🔨 Builder Agent: Creating agent "${agentSpec.Name}"...`);
50
+
51
+ // Create AgentSpecSync instance
52
+ const specSync = new AgentSpecSync(agentSpec, params.contextUser);
53
+
54
+ // Mark as dirty to ensure it saves
55
+ specSync.markDirty();
56
+
57
+ // Save to database
58
+ const agentId = await specSync.SaveToDatabase();
59
+
60
+ console.log(`✅ Builder Agent: Successfully created agent with ID: ${agentId}`);
61
+
62
+ // Return success with the created agent ID
63
+ const updatedSpec = { ...agentSpec, ID: agentId };
64
+
65
+ return {
66
+ finalStep: {
67
+ terminate: true,
68
+ step: 'Success',
69
+ reasoning: `Successfully created agent "${agentSpec.Name}" with ID: ${agentId}`,
70
+ newPayload: updatedSpec as P
71
+ },
72
+ stepCount: 1
73
+ };
74
+
75
+ } catch (error: any) {
76
+ console.error('❌ Builder Agent: Failed to create agent:', error);
77
+
78
+ // Return failure with error details
79
+ return {
80
+ finalStep: {
81
+ terminate: true,
82
+ step: 'Failed',
83
+ reasoning: `Failed to create agent: ${error?.message || String(error)}`
84
+ },
85
+ stepCount: 1
86
+ };
87
+ }
88
+ }
89
+ }