@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -0
- package/dist/agent-spec-sync.d.ts +18 -3
- package/dist/agent-spec-sync.d.ts.map +1 -1
- package/dist/agent-spec-sync.js +129 -27
- package/dist/agent-spec-sync.js.map +1 -1
- package/dist/agents/architect-agent.d.ts +52 -0
- package/dist/agents/architect-agent.d.ts.map +1 -0
- package/dist/agents/architect-agent.js +247 -0
- package/dist/agents/architect-agent.js.map +1 -0
- package/dist/agents/builder-agent.d.ts +29 -0
- package/dist/agents/builder-agent.d.ts.map +1 -0
- package/dist/agents/builder-agent.js +84 -0
- package/dist/agents/builder-agent.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/agent-spec-sync.ts +158 -32
- package/src/agents/architect-agent.ts +293 -0
- package/src/agents/builder-agent.ts +89 -0
- package/src/index.ts +21 -2
- package/tsconfig.json +3 -1
package/src/agent-spec-sync.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
659
|
-
* child agent
|
|
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
|
|
678
|
+
* @throws {Error} If child agent save fails
|
|
665
679
|
*/
|
|
666
680
|
private async saveChildSubAgent(parentId: string, SubAgentSpec: SubAgentSpec): Promise<void> {
|
|
667
|
-
|
|
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
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
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.
|
|
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.
|
|
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 ||
|
|
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
|
+
}
|