@memberjunction/ai-agent-manager 2.111.0 → 2.112.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.
@@ -1,7 +1,7 @@
1
1
  import { BaseAgent, PayloadManager } from '@memberjunction/ai-agents';
2
2
  import { ExecuteAgentParams, BaseAgentNextStep, AgentSpec } from '@memberjunction/ai-core-plus';
3
3
  import { AIAgentRunEntityExtended, AIAgentRunStepEntityExtended, ActionEntity, AIPromptEntity } from '@memberjunction/core-entities';
4
- import { Metadata, RunView } from '@memberjunction/core';
4
+ import { Metadata, RunView } from '@memberjunction/global';
5
5
  import { RegisterClass } from '@memberjunction/global';
6
6
 
7
7
  /**
@@ -22,435 +22,442 @@ import { RegisterClass } from '@memberjunction/global';
22
22
  */
23
23
  @RegisterClass(BaseAgent, 'AgentArchitectAgent')
24
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;
25
+ /**
26
+ * Applies payload changes if any are requested in the next step
27
+ */
28
+ protected applyPayloadChanges<P>(payload: P, nextStep: BaseAgentNextStep<P>): P {
29
+ if (nextStep.payloadChangeRequest) {
30
+ const pm = new PayloadManager();
31
+ const result = pm.applyAgentChangeRequest<P>(payload, nextStep.payloadChangeRequest);
32
+ return result.result || payload;
33
+ }
34
+ return payload;
35
+ }
36
+
37
+ protected override async validateSuccessNextStep<P>(
38
+ params: ExecuteAgentParams,
39
+ nextStep: BaseAgentNextStep<P>,
40
+ currentPayload: P,
41
+ agentRun: AIAgentRunEntityExtended,
42
+ currentStep: AIAgentRunStepEntityExtended
43
+ ): Promise<BaseAgentNextStep<P>> {
44
+ // First call base validation
45
+ const baseValidation = await super.validateSuccessNextStep(params, nextStep, currentPayload, agentRun, currentStep);
46
+ if (baseValidation.step === 'Retry') {
47
+ return baseValidation;
36
48
  }
37
49
 
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
+ // Apply payload changes to get the spec that would be sent forward
51
+ const payloadWithChanges = this.applyPayloadChanges<P>(currentPayload, nextStep);
50
52
 
51
- // Apply payload changes to get the spec that would be sent forward
52
- const payloadWithChanges = this.applyPayloadChanges<P>(currentPayload, nextStep);
53
+ // The payload IS the AgentSpec
54
+ const agentSpec = payloadWithChanges as AgentSpec;
53
55
 
54
- // The payload IS the AgentSpec
55
- const agentSpec = payloadWithChanges as AgentSpec;
56
+ console.log('🏗️ Architect Agent: Validating AgentSpec:', agentSpec ? `Name="${agentSpec.Name}"` : 'agentSpec is null/undefined');
56
57
 
57
- console.log('🏗️ Architect Agent: Validating AgentSpec:', agentSpec ? `Name="${agentSpec.Name}"` : 'agentSpec is null/undefined');
58
+ // Validate the AgentSpec
59
+ const validation = await this.validateAgentSpec(agentSpec, params);
58
60
 
59
- // Validate the AgentSpec
60
- const validation = await this.validateAgentSpec(agentSpec, params);
61
+ if (validation.errors.length > 0) {
62
+ console.log('❌ Architect Agent: Validation failed with errors:', validation.errors);
63
+ // Return retry with detailed error messages
64
+ return {
65
+ ...nextStep,
66
+ step: 'Retry',
67
+ retryInstructions: this.formatValidationErrors(validation.errors),
68
+ newPayload: nextStep.newPayload,
69
+ };
70
+ }
61
71
 
62
- if (validation.errors.length > 0) {
63
- console.log('❌ Architect Agent: Validation failed with errors:', validation.errors);
64
- // Return retry with detailed error messages
65
- return {
66
- ...nextStep,
67
- step: 'Retry',
68
- retryInstructions: this.formatValidationErrors(validation.errors),
69
- newPayload: nextStep.newPayload
70
- };
71
- }
72
+ // If spec was auto-corrected, update the payload with corrected spec
73
+ if (validation.correctedSpec) {
74
+ console.log('✓ Architect Agent: Auto-corrected AgentSpec, updating payload');
75
+ return {
76
+ ...nextStep,
77
+ newPayload: validation.correctedSpec as P,
78
+ };
79
+ }
72
80
 
73
- // If spec was auto-corrected, update the payload with corrected spec
74
- if (validation.correctedSpec) {
75
- console.log('✓ Architect Agent: Auto-corrected AgentSpec, updating payload');
76
- return {
77
- ...nextStep,
78
- newPayload: validation.correctedSpec as P
79
- };
80
- }
81
+ console.log('✅ Architect Agent: AgentSpec validation passed');
82
+ return nextStep;
83
+ }
84
+
85
+ /**
86
+ * Validates an AgentSpec object
87
+ * Returns errors array and potentially a corrected spec
88
+ */
89
+ protected async validateAgentSpec(spec: AgentSpec, params: ExecuteAgentParams): Promise<{ errors: string[]; correctedSpec?: AgentSpec }> {
90
+ const errors: string[] = [];
91
+ let corrected = false;
92
+
93
+ // Check if spec exists
94
+ if (!spec) {
95
+ errors.push(
96
+ '❌ AgentSpec is null or undefined. Ensure you place the AgentSpec in payload.agentSpec (inside a payloadChangeRequest with newElements or replaceElements)'
97
+ );
98
+ return { errors };
99
+ }
81
100
 
82
- console.log('✅ Architect Agent: AgentSpec validation passed');
83
- return nextStep;
101
+ const correctedSpec = { ...spec };
102
+
103
+ // 1. Validate required fields
104
+ if (!correctedSpec.Name || correctedSpec.Name.trim().length === 0) {
105
+ errors.push('❌ AgentSpec.Name is required and cannot be empty');
84
106
  }
85
107
 
86
- /**
87
- * Validates an AgentSpec object
88
- * Returns errors array and potentially a corrected spec
89
- */
90
- protected async validateAgentSpec(
91
- spec: AgentSpec,
92
- params: ExecuteAgentParams
93
- ): Promise<{ errors: string[]; correctedSpec?: AgentSpec }> {
94
- const errors: string[] = [];
95
- let corrected = false;
96
-
97
- // Check if spec exists
98
- if (!spec) {
99
- errors.push('❌ AgentSpec is null or undefined. Ensure you place the AgentSpec in payload.agentSpec (inside a payloadChangeRequest with newElements or replaceElements)');
100
- return { errors };
101
- }
108
+ if (!correctedSpec.StartingPayloadValidationMode) {
109
+ // Auto-correct: add default
110
+ correctedSpec.StartingPayloadValidationMode = 'Fail';
111
+ corrected = true;
112
+ console.log('✓ Auto-corrected: Added default StartingPayloadValidationMode = "Fail"');
113
+ } else if (correctedSpec.StartingPayloadValidationMode !== 'Fail' && correctedSpec.StartingPayloadValidationMode !== 'Warn') {
114
+ errors.push(
115
+ `❌ StartingPayloadValidationMode must be either "Fail" or "Warn", got: "${correctedSpec.StartingPayloadValidationMode}"`
116
+ );
117
+ }
102
118
 
103
- const correctedSpec = { ...spec };
119
+ // 2. Validate Actions if specified
120
+ if (correctedSpec.Actions && correctedSpec.Actions.length > 0) {
121
+ const actionValidation = await this.validateActions(correctedSpec.Actions, params);
122
+ if (actionValidation.errors.length > 0) {
123
+ errors.push(...actionValidation.errors);
124
+ }
125
+ }
104
126
 
105
- // 1. Validate required fields
106
- if (!correctedSpec.Name || correctedSpec.Name.trim().length === 0) {
107
- errors.push('❌ AgentSpec.Name is required and cannot be empty');
108
- }
127
+ // 3. Validate SubAgents if specified
128
+ if (correctedSpec.SubAgents && correctedSpec.SubAgents.length > 0) {
129
+ const subAgentValidation = this.validateSubAgents(correctedSpec.SubAgents);
130
+ if (subAgentValidation.errors.length > 0) {
131
+ errors.push(...subAgentValidation.errors);
132
+ }
133
+ if (subAgentValidation.corrected) {
134
+ corrected = true;
135
+ }
136
+ }
109
137
 
110
- if (!correctedSpec.StartingPayloadValidationMode) {
111
- // Auto-correct: add default
112
- correctedSpec.StartingPayloadValidationMode = 'Fail';
113
- corrected = true;
114
- console.log('✓ Auto-corrected: Added default StartingPayloadValidationMode = "Fail"');
115
- } else if (correctedSpec.StartingPayloadValidationMode !== 'Fail' && correctedSpec.StartingPayloadValidationMode !== 'Warn') {
116
- errors.push(`❌ StartingPayloadValidationMode must be either "Fail" or "Warn", got: "${correctedSpec.StartingPayloadValidationMode}"`);
117
- }
138
+ // 4. Validate TypeID and Status
139
+ const specWithType = correctedSpec as any;
140
+ if (!specWithType.TypeID) {
141
+ errors.push('❌ TypeID is required - must be "@lookup:MJ: AI Agent Types.Name=Loop" or "@lookup:MJ: AI Agent Types.Name=Flow"');
142
+ }
118
143
 
119
- // 2. Validate Actions if specified
120
- if (correctedSpec.Actions && correctedSpec.Actions.length > 0) {
121
- const actionValidation = await this.validateActions(correctedSpec.Actions, params);
122
- if (actionValidation.errors.length > 0) {
123
- errors.push(...actionValidation.errors);
124
- }
125
- }
144
+ if (!specWithType.Status) {
145
+ // Auto-correct: add default
146
+ specWithType.Status = 'Active';
147
+ corrected = true;
148
+ console.log('✓ Auto-corrected: Added default Status = "Active"');
149
+ } else if (!['Active', 'Inactive', 'Pending'].includes(specWithType.Status)) {
150
+ errors.push(`❌ Status must be "Active", "Inactive", or "Pending", got: "${specWithType.Status}"`);
151
+ }
126
152
 
127
- // 3. Validate SubAgents if specified
128
- if (correctedSpec.SubAgents && correctedSpec.SubAgents.length > 0) {
129
- const subAgentValidation = this.validateSubAgents(correctedSpec.SubAgents);
130
- if (subAgentValidation.errors.length > 0) {
131
- errors.push(...subAgentValidation.errors);
132
- }
133
- if (subAgentValidation.corrected) {
134
- corrected = true;
135
- }
136
- }
153
+ // 5. Validate agent type-specific requirements
154
+ const isLoopAgent = specWithType.TypeID?.includes('Loop');
155
+ const isFlowAgent = specWithType.TypeID?.includes('Flow');
137
156
 
138
- // 4. Validate TypeID and Status
139
- const specWithType = correctedSpec as any;
140
- if (!specWithType.TypeID) {
141
- errors.push('❌ TypeID is required - must be "@lookup:MJ: AI Agent Types.Name=Loop" or "@lookup:MJ: AI Agent Types.Name=Flow"');
142
- }
157
+ if (isLoopAgent) {
158
+ // Loop agents require at least one prompt
159
+ if (!correctedSpec.Prompts || correctedSpec.Prompts.length === 0) {
160
+ errors.push('❌ Loop agents require at least ONE prompt in the Prompts array');
161
+ }
162
+ }
143
163
 
144
- if (!specWithType.Status) {
145
- // Auto-correct: add default
146
- specWithType.Status = 'Active';
147
- corrected = true;
148
- console.log('✓ Auto-corrected: Added default Status = "Active"');
149
- } else if (!['Active', 'Inactive', 'Pending'].includes(specWithType.Status)) {
150
- errors.push(`❌ Status must be "Active", "Inactive", or "Pending", got: "${specWithType.Status}"`);
164
+ if (isFlowAgent) {
165
+ // CRITICAL: Flow agents MUST have empty Prompts array (no agent-level prompts)
166
+ if (correctedSpec.Prompts && correctedSpec.Prompts.length > 0) {
167
+ errors.push(
168
+ '❌ Flow agents MUST have empty Prompts array. If LLM is needed, use a Prompt step OR a Loop sub-agent instead of agent-level prompts.'
169
+ );
170
+ }
171
+
172
+ // Flow agents require Steps and Paths
173
+ if (!correctedSpec.Steps || correctedSpec.Steps.length === 0) {
174
+ errors.push('❌ Flow agents require at least ONE step in the Steps array');
175
+ }
176
+ // Paths are technically optional (single-step flows), but validate if present
177
+ if (correctedSpec.Steps && correctedSpec.Steps.length > 1) {
178
+ if (!correctedSpec.Paths || correctedSpec.Paths.length === 0) {
179
+ errors.push('❌ Flow agents with multiple steps require at least ONE path in the Paths array');
151
180
  }
181
+ }
152
182
 
153
- // 5. Validate agent type-specific requirements
154
- const isLoopAgent = specWithType.TypeID?.includes('Loop');
155
- const isFlowAgent = specWithType.TypeID?.includes('Flow');
156
-
157
- if (isLoopAgent) {
158
- // Loop agents require at least one prompt
159
- if (!correctedSpec.Prompts || correctedSpec.Prompts.length === 0) {
160
- errors.push('❌ Loop agents require at least ONE prompt in the Prompts array');
161
- }
183
+ // Validate that at least one step is marked as StartingStep
184
+ if (correctedSpec.Steps && correctedSpec.Steps.length > 0) {
185
+ const hasStartingStep = correctedSpec.Steps.some((step) => step.StartingStep === true);
186
+ if (!hasStartingStep) {
187
+ errors.push('❌ Flow agents require at least ONE step with StartingStep: true');
162
188
  }
163
189
 
164
- if (isFlowAgent) {
165
- // CRITICAL: Flow agents MUST have empty Prompts array (no agent-level prompts)
166
- if (correctedSpec.Prompts && correctedSpec.Prompts.length > 0) {
167
- errors.push('❌ Flow agents MUST have empty Prompts array. If LLM is needed, use a Prompt step OR a Loop sub-agent instead of agent-level prompts.');
168
- }
190
+ // Validate each step based on its type
191
+ for (let i = 0; i < correctedSpec.Steps.length; i++) {
192
+ const step = correctedSpec.Steps[i];
169
193
 
170
- // Flow agents require Steps and Paths
171
- if (!correctedSpec.Steps || correctedSpec.Steps.length === 0) {
172
- errors.push('❌ Flow agents require at least ONE step in the Steps array');
173
- }
174
- // Paths are technically optional (single-step flows), but validate if present
175
- if (correctedSpec.Steps && correctedSpec.Steps.length > 1) {
176
- if (!correctedSpec.Paths || correctedSpec.Paths.length === 0) {
177
- errors.push('❌ Flow agents with multiple steps require at least ONE path in the Paths array');
178
- }
194
+ // Validate Action steps
195
+ if (step.StepType === 'Action') {
196
+ if (!step.ActionID) {
197
+ errors.push(`❌ Action step "${step.Name}" (index ${i}) must have ActionID field`);
179
198
  }
180
199
 
181
- // Validate that at least one step is marked as StartingStep
182
- if (correctedSpec.Steps && correctedSpec.Steps.length > 0) {
183
- const hasStartingStep = correctedSpec.Steps.some(step => step.StartingStep === true);
184
- if (!hasStartingStep) {
185
- errors.push('❌ Flow agents require at least ONE step with StartingStep: true');
200
+ // Validate ActionInputMapping if provided (supports both string and object)
201
+ if (step.ActionInputMapping) {
202
+ try {
203
+ if (typeof step.ActionInputMapping === 'string') {
204
+ JSON.parse(step.ActionInputMapping);
186
205
  }
206
+ // else it's already an object, which is valid and will be stringified later
207
+ } catch (e: any) {
208
+ errors.push(`❌ Step "${step.Name}" (index ${i}) has invalid ActionInputMapping JSON: ${e?.message || String(e)}`);
209
+ }
210
+ }
187
211
 
188
- // Validate each step based on its type
189
- for (let i = 0; i < correctedSpec.Steps.length; i++) {
190
- const step = correctedSpec.Steps[i];
191
-
192
- // Validate Action steps
193
- if (step.StepType === 'Action') {
194
- if (!step.ActionID) {
195
- errors.push(`❌ Action step "${step.Name}" (index ${i}) must have ActionID field`);
196
- }
197
-
198
- // Validate ActionInputMapping if provided (supports both string and object)
199
- if (step.ActionInputMapping) {
200
- try {
201
- if (typeof step.ActionInputMapping === 'string') {
202
- JSON.parse(step.ActionInputMapping);
203
- }
204
- // else it's already an object, which is valid and will be stringified later
205
- } catch (e: any) {
206
- errors.push(`❌ Step "${step.Name}" (index ${i}) has invalid ActionInputMapping JSON: ${e?.message || String(e)}`);
207
- }
208
- }
209
-
210
- // Validate ActionOutputMapping if provided (supports both string and object)
211
- if (step.ActionOutputMapping) {
212
- try {
213
- if (typeof step.ActionOutputMapping === 'string') {
214
- JSON.parse(step.ActionOutputMapping);
215
- }
216
- // else it's already an object, which is valid and will be stringified later
217
- } catch (e: any) {
218
- errors.push(`❌ Step "${step.Name}" (index ${i}) has invalid ActionOutputMapping JSON: ${e?.message || String(e)}`);
219
- }
220
- }
221
- }
222
-
223
- // Validate Prompt steps
224
- if (step.StepType === 'Prompt') {
225
- // If PromptID is empty or not provided, PromptText is required for inline creation
226
- if (!step.PromptID || step.PromptID === '') {
227
- if (!step.PromptText || step.PromptText.trim() === '') {
228
- errors.push(`❌ Prompt step "${step.Name}" (index ${i}) has empty PromptID but missing PromptText. For inline prompt creation, PromptText is required.`);
229
- }
230
- // PromptName and PromptDescription are recommended but not required
231
- if (!step.PromptName) {
232
- console.log(`⚠️ Warning: Prompt step "${step.Name}" (index ${i}) is missing PromptName (will default to "[Step Name] Prompt")`);
233
- }
234
- }
235
- // If PromptID is provided (existing prompt), other fields are optional
236
- }
237
-
238
- // Validate Sub-Agent steps
239
- if (step.StepType === 'Sub-Agent') {
240
- // SubAgentID can be empty "" for new sub-agents (will be linked by name matching)
241
- // No validation needed here - linking happens in AgentSpecSync
242
- }
212
+ // Validate ActionOutputMapping if provided (supports both string and object)
213
+ if (step.ActionOutputMapping) {
214
+ try {
215
+ if (typeof step.ActionOutputMapping === 'string') {
216
+ JSON.parse(step.ActionOutputMapping);
243
217
  }
218
+ // else it's already an object, which is valid and will be stringified later
219
+ } catch (e: any) {
220
+ errors.push(`❌ Step "${step.Name}" (index ${i}) has invalid ActionOutputMapping JSON: ${e?.message || String(e)}`);
221
+ }
244
222
  }
223
+ }
224
+
225
+ // Validate Prompt steps
226
+ if (step.StepType === 'Prompt') {
227
+ // If PromptID is empty or not provided, PromptText is required for inline creation
228
+ if (!step.PromptID || step.PromptID === '') {
229
+ if (!step.PromptText || step.PromptText.trim() === '') {
230
+ errors.push(
231
+ `❌ Prompt step "${step.Name}" (index ${i}) has empty PromptID but missing PromptText. For inline prompt creation, PromptText is required.`
232
+ );
233
+ }
234
+ // PromptName and PromptDescription are recommended but not required
235
+ if (!step.PromptName) {
236
+ console.log(
237
+ `⚠️ Warning: Prompt step "${step.Name}" (index ${i}) is missing PromptName (will default to "[Step Name] Prompt")`
238
+ );
239
+ }
240
+ }
241
+ // If PromptID is provided (existing prompt), other fields are optional
242
+ }
243
+
244
+ // Validate Sub-Agent steps
245
+ if (step.StepType === 'Sub-Agent') {
246
+ // SubAgentID can be empty "" for new sub-agents (will be linked by name matching)
247
+ // No validation needed here - linking happens in AgentSpecSync
248
+ }
245
249
  }
250
+ }
251
+ }
246
252
 
247
- // 6. Validate optional but important fields
248
- if (correctedSpec.FinalPayloadValidationMode &&
249
- !['Fail', 'Retry', 'Warn'].includes(correctedSpec.FinalPayloadValidationMode)) {
250
- errors.push(`❌ FinalPayloadValidationMode must be "Fail", "Retry", or "Warn", got: "${correctedSpec.FinalPayloadValidationMode}"`);
251
- }
252
-
253
- if (correctedSpec.InvocationMode &&
254
- !['Any', 'Agent', 'User', 'Never'].includes(correctedSpec.InvocationMode)) {
255
- errors.push(`❌ InvocationMode must be "Any", "Agent", "User", or "Never", got: "${correctedSpec.InvocationMode}"`);
256
- }
253
+ // 6. Validate optional but important fields
254
+ if (correctedSpec.FinalPayloadValidationMode && !['Fail', 'Retry', 'Warn'].includes(correctedSpec.FinalPayloadValidationMode)) {
255
+ errors.push(`❌ FinalPayloadValidationMode must be "Fail", "Retry", or "Warn", got: "${correctedSpec.FinalPayloadValidationMode}"`);
256
+ }
257
257
 
258
- return {
259
- errors,
260
- correctedSpec: corrected ? correctedSpec : undefined
261
- };
258
+ if (correctedSpec.InvocationMode && !['Any', 'Agent', 'User', 'Never'].includes(correctedSpec.InvocationMode)) {
259
+ errors.push(`❌ InvocationMode must be "Any", "Agent", "User", or "Never", got: "${correctedSpec.InvocationMode}"`);
262
260
  }
263
261
 
264
- /**
265
- * Validates that action IDs exist in the database
266
- */
267
- protected async validateActions(
268
- actions: AgentSpec['Actions'],
269
- params: ExecuteAgentParams
270
- ): Promise<{ errors: string[] }> {
271
- const errors: string[] = [];
272
-
273
- if (!actions || actions.length === 0) {
274
- return { errors };
275
- }
262
+ return {
263
+ errors,
264
+ correctedSpec: corrected ? correctedSpec : undefined,
265
+ };
266
+ }
276
267
 
277
- try {
278
- const rv = new RunView();
279
- const actionIds = actions
280
- .map(a => a.ActionID)
281
- .filter(id => id && id.trim().length > 0);
268
+ /**
269
+ * Validates that action IDs exist in the database
270
+ */
271
+ protected async validateActions(actions: AgentSpec['Actions'], params: ExecuteAgentParams): Promise<{ errors: string[] }> {
272
+ const errors: string[] = [];
282
273
 
283
- if (actionIds.length === 0) {
284
- errors.push('❌ All actions must have an ActionID');
285
- return { errors };
286
- }
287
-
288
- // Query database for these action IDs
289
- const filter = actionIds.map(id => `ID='${id}'`).join(' OR ');
290
- const result = await rv.RunView<ActionEntity>({
291
- EntityName: 'Actions',
292
- ExtraFilter: filter,
293
- ResultType: 'entity_object'
294
- }, params.contextUser);
295
-
296
- if (!result.Success) {
297
- errors.push(`❌ Failed to validate actions: ${result.ErrorMessage}`);
298
- return { errors };
299
- }
274
+ if (!actions || actions.length === 0) {
275
+ return { errors };
276
+ }
300
277
 
301
- const foundIds = new Set(result.Results.map(a => a.ID));
278
+ try {
279
+ const rv = new RunView();
280
+ const actionIds = actions.map((a) => a.ActionID).filter((id) => id && id.trim().length > 0);
302
281
 
303
- // Check which actions weren't found
304
- for (const action of actions) {
305
- if (!action.ActionID) {
306
- errors.push('❌ Action is missing ActionID field');
307
- continue;
308
- }
282
+ if (actionIds.length === 0) {
283
+ errors.push('❌ All actions must have an ActionID');
284
+ return { errors };
285
+ }
286
+
287
+ // Query database for these action IDs
288
+ const filter = actionIds.map((id) => `ID='${id}'`).join(' OR ');
289
+ const result = await rv.RunView<ActionEntity>(
290
+ {
291
+ EntityName: 'Actions',
292
+ ExtraFilter: filter,
293
+ ResultType: 'entity_object',
294
+ },
295
+ params.contextUser
296
+ );
297
+
298
+ if (!result.Success) {
299
+ errors.push(`❌ Failed to validate actions: ${result.ErrorMessage}`);
300
+ return { errors };
301
+ }
309
302
 
310
- if (!foundIds.has(action.ActionID)) {
311
- errors.push(`❌ Action with ID "${action.ActionID}" not found in database. Please use "Find Candidate Actions" or "List Actions" to get valid action IDs.`);
312
- }
313
- }
303
+ const foundIds = new Set(result.Results.map((a) => a.ID));
314
304
 
315
- } catch (error: any) {
316
- errors.push(`❌ Error validating actions: ${error?.message || String(error)}`);
305
+ // Check which actions weren't found
306
+ for (const action of actions) {
307
+ if (!action.ActionID) {
308
+ errors.push('❌ Action is missing ActionID field');
309
+ continue;
317
310
  }
318
311
 
319
- return { errors };
320
- }
321
-
322
- /**
323
- * Validates SubAgent structure recursively
324
- */
325
- protected validateSubAgents(
326
- subAgents: AgentSpec['SubAgents']
327
- ): { errors: string[]; corrected: boolean } {
328
- const errors: string[] = [];
329
- let corrected = false;
330
-
331
- if (!subAgents || subAgents.length === 0) {
332
- return { errors, corrected };
312
+ if (!foundIds.has(action.ActionID)) {
313
+ errors.push(
314
+ `❌ Action with ID "${action.ActionID}" not found in database. Please use "Find Candidate Actions" or "List Actions" to get valid action IDs.`
315
+ );
333
316
  }
317
+ }
318
+ } catch (error: any) {
319
+ errors.push(`❌ Error validating actions: ${error?.message || String(error)}`);
320
+ }
334
321
 
335
- for (let i = 0; i < subAgents.length; i++) {
336
- const subAgent = subAgents[i];
337
-
338
- // Validate Type field
339
- if (!subAgent.Type) {
340
- errors.push(`❌ SubAgent at index ${i} is missing required "Type" field (must be "child" or "related")`);
341
- continue;
342
- }
343
-
344
- if (subAgent.Type !== 'child' && subAgent.Type !== 'related') {
345
- errors.push(`❌ SubAgent at index ${i} has invalid Type "${subAgent.Type}" (must be "child" or "related")`);
346
- continue;
347
- }
348
-
349
- // Validate SubAgent.SubAgent field
350
- if (!subAgent.SubAgent) {
351
- errors.push(`❌ SubAgent at index ${i} is missing required "SubAgent" field`);
352
- continue;
353
- }
354
-
355
- // SubAgent.ID can be empty string "" for NEW sub-agents (Builder creates them recursively)
356
- // Only validate that ID is not null/undefined
357
- if (subAgent.SubAgent.ID === null || subAgent.SubAgent.ID === undefined) {
358
- errors.push(`❌ SubAgent at index ${i} has null/undefined SubAgent.ID (use empty string "" for new sub-agents, or provide existing agent GUID)`);
359
- }
360
-
361
- // Validate SubAgent.SubAgent.Name
362
- if (!subAgent.SubAgent.Name || subAgent.SubAgent.Name.trim().length === 0) {
363
- errors.push(`❌ SubAgent at index ${i} is missing SubAgent.Name`);
364
- }
365
-
366
- // Validate TypeID for sub-agent
367
- if (!subAgent.SubAgent.TypeID) {
368
- errors.push(`❌ SubAgent[${i}] "${subAgent.SubAgent.Name}" is missing TypeID field`);
369
- } else {
370
- // Validate Loop/Flow specific requirements for sub-agents
371
- const isLoopSubAgent = subAgent.SubAgent.TypeID.includes('Loop');
372
- const isFlowSubAgent = subAgent.SubAgent.TypeID.includes('Flow');
373
-
374
- if (isLoopSubAgent) {
375
- // Loop sub-agents require at least one prompt
376
- if (!subAgent.SubAgent.Prompts || subAgent.SubAgent.Prompts.length === 0) {
377
- errors.push(`❌ Loop SubAgent[${i}] "${subAgent.SubAgent.Name}" requires at least ONE prompt in Prompts array`);
378
- }
379
- }
380
-
381
- if (isFlowSubAgent) {
382
- // Flow sub-agents MUST have empty Prompts array
383
- if (subAgent.SubAgent.Prompts && subAgent.SubAgent.Prompts.length > 0) {
384
- errors.push(`❌ Flow SubAgent[${i}] "${subAgent.SubAgent.Name}" MUST have empty Prompts array. Use Prompt steps or Loop sub-agents for LLM functionality.`);
385
- }
386
-
387
- // Flow sub-agents require Steps
388
- if (!subAgent.SubAgent.Steps || subAgent.SubAgent.Steps.length === 0) {
389
- errors.push(`❌ Flow SubAgent[${i}] "${subAgent.SubAgent.Name}" requires at least ONE step in Steps array`);
390
- }
391
- }
392
- }
393
-
394
- // Auto-correct: Add default StartingPayloadValidationMode if missing
395
- if (!subAgent.SubAgent.StartingPayloadValidationMode) {
396
- subAgent.SubAgent.StartingPayloadValidationMode = 'Fail';
397
- corrected = true;
398
- console.log(`✓ Auto-corrected: Added SubAgent[${i}].SubAgent.StartingPayloadValidationMode = "Fail"`);
399
- }
400
-
401
- // Auto-correct: Add default Status if missing
402
- if (!subAgent.SubAgent.Status) {
403
- subAgent.SubAgent.Status = 'Active';
404
- corrected = true;
405
- console.log(`✓ Auto-corrected: Added SubAgent[${i}].SubAgent.Status = "Active"`);
406
- }
322
+ return { errors };
323
+ }
407
324
 
408
- // Validate related-specific fields
409
- if (subAgent.Type === 'related') {
410
- // AgentRelationshipID can be empty for new relationships, it will be created on save
325
+ /**
326
+ * Validates SubAgent structure recursively
327
+ */
328
+ protected validateSubAgents(subAgents: AgentSpec['SubAgents']): { errors: string[]; corrected: boolean } {
329
+ const errors: string[] = [];
330
+ let corrected = false;
411
331
 
412
- // IMPORTANT: Related agents REQUIRE existing agent ID
413
- if (!subAgent.SubAgent.ID || subAgent.SubAgent.ID === '') {
414
- errors.push(`❌ Related SubAgent[${i}] "${subAgent.SubAgent.Name}" must have existing agent ID. Use Find Candidate Agents to get the ID.`);
415
- }
416
-
417
- // IMPORTANT: Related agents REQUIRE mapping fields
418
- if (!subAgent.SubAgentInputMapping && !subAgent.SubAgentOutputMapping) {
419
- errors.push(`❌ Related SubAgent[${i}] "${subAgent.SubAgent.Name}" must have at least SubAgentInputMapping or SubAgentOutputMapping`);
420
- }
421
- }
332
+ if (!subAgents || subAgents.length === 0) {
333
+ return { errors, corrected };
334
+ }
422
335
 
423
- // Recursively validate nested sub-agents
424
- if (subAgent.SubAgent.SubAgents && subAgent.SubAgent.SubAgents.length > 0) {
425
- const nestedValidation = this.validateSubAgents(subAgent.SubAgent.SubAgents);
426
- if (nestedValidation.errors.length > 0) {
427
- // Prefix nested errors with parent context
428
- const prefixedErrors = nestedValidation.errors.map(
429
- err => `SubAgent[${i}] "${subAgent.SubAgent.Name}" -> ${err}`
430
- );
431
- errors.push(...prefixedErrors);
432
- }
433
- if (nestedValidation.corrected) {
434
- corrected = true;
435
- }
436
- }
336
+ for (let i = 0; i < subAgents.length; i++) {
337
+ const subAgent = subAgents[i];
338
+
339
+ // Validate Type field
340
+ if (!subAgent.Type) {
341
+ errors.push(`❌ SubAgent at index ${i} is missing required "Type" field (must be "child" or "related")`);
342
+ continue;
343
+ }
344
+
345
+ if (subAgent.Type !== 'child' && subAgent.Type !== 'related') {
346
+ errors.push(`❌ SubAgent at index ${i} has invalid Type "${subAgent.Type}" (must be "child" or "related")`);
347
+ continue;
348
+ }
349
+
350
+ // Validate SubAgent.SubAgent field
351
+ if (!subAgent.SubAgent) {
352
+ errors.push(`❌ SubAgent at index ${i} is missing required "SubAgent" field`);
353
+ continue;
354
+ }
355
+
356
+ // SubAgent.ID can be empty string "" for NEW sub-agents (Builder creates them recursively)
357
+ // Only validate that ID is not null/undefined
358
+ if (subAgent.SubAgent.ID === null || subAgent.SubAgent.ID === undefined) {
359
+ errors.push(
360
+ `❌ SubAgent at index ${i} has null/undefined SubAgent.ID (use empty string "" for new sub-agents, or provide existing agent GUID)`
361
+ );
362
+ }
363
+
364
+ // Validate SubAgent.SubAgent.Name
365
+ if (!subAgent.SubAgent.Name || subAgent.SubAgent.Name.trim().length === 0) {
366
+ errors.push(`❌ SubAgent at index ${i} is missing SubAgent.Name`);
367
+ }
368
+
369
+ // Validate TypeID for sub-agent
370
+ if (!subAgent.SubAgent.TypeID) {
371
+ errors.push(`❌ SubAgent[${i}] "${subAgent.SubAgent.Name}" is missing TypeID field`);
372
+ } else {
373
+ // Validate Loop/Flow specific requirements for sub-agents
374
+ const isLoopSubAgent = subAgent.SubAgent.TypeID.includes('Loop');
375
+ const isFlowSubAgent = subAgent.SubAgent.TypeID.includes('Flow');
376
+
377
+ if (isLoopSubAgent) {
378
+ // Loop sub-agents require at least one prompt
379
+ if (!subAgent.SubAgent.Prompts || subAgent.SubAgent.Prompts.length === 0) {
380
+ errors.push(`❌ Loop SubAgent[${i}] "${subAgent.SubAgent.Name}" requires at least ONE prompt in Prompts array`);
381
+ }
437
382
  }
438
383
 
439
- return { errors, corrected };
440
- }
384
+ if (isFlowSubAgent) {
385
+ // Flow sub-agents MUST have empty Prompts array
386
+ if (subAgent.SubAgent.Prompts && subAgent.SubAgent.Prompts.length > 0) {
387
+ errors.push(
388
+ `❌ Flow SubAgent[${i}] "${subAgent.SubAgent.Name}" MUST have empty Prompts array. Use Prompt steps or Loop sub-agents for LLM functionality.`
389
+ );
390
+ }
391
+
392
+ // Flow sub-agents require Steps
393
+ if (!subAgent.SubAgent.Steps || subAgent.SubAgent.Steps.length === 0) {
394
+ errors.push(`❌ Flow SubAgent[${i}] "${subAgent.SubAgent.Name}" requires at least ONE step in Steps array`);
395
+ }
396
+ }
397
+ }
398
+
399
+ // Auto-correct: Add default StartingPayloadValidationMode if missing
400
+ if (!subAgent.SubAgent.StartingPayloadValidationMode) {
401
+ subAgent.SubAgent.StartingPayloadValidationMode = 'Fail';
402
+ corrected = true;
403
+ console.log(`✓ Auto-corrected: Added SubAgent[${i}].SubAgent.StartingPayloadValidationMode = "Fail"`);
404
+ }
405
+
406
+ // Auto-correct: Add default Status if missing
407
+ if (!subAgent.SubAgent.Status) {
408
+ subAgent.SubAgent.Status = 'Active';
409
+ corrected = true;
410
+ console.log(`✓ Auto-corrected: Added SubAgent[${i}].SubAgent.Status = "Active"`);
411
+ }
412
+
413
+ // Validate related-specific fields
414
+ if (subAgent.Type === 'related') {
415
+ // AgentRelationshipID can be empty for new relationships, it will be created on save
416
+
417
+ // IMPORTANT: Related agents REQUIRE existing agent ID
418
+ if (!subAgent.SubAgent.ID || subAgent.SubAgent.ID === '') {
419
+ errors.push(
420
+ `❌ Related SubAgent[${i}] "${subAgent.SubAgent.Name}" must have existing agent ID. Use Find Candidate Agents to get the ID.`
421
+ );
422
+ }
441
423
 
442
- /**
443
- * Formats validation errors into a clear, actionable message
444
- */
445
- protected formatValidationErrors(errors: string[]): string {
446
- if (errors.length === 0) {
447
- return '';
424
+ // IMPORTANT: Related agents REQUIRE mapping fields
425
+ if (!subAgent.SubAgentInputMapping && !subAgent.SubAgentOutputMapping) {
426
+ errors.push(
427
+ `❌ Related SubAgent[${i}] "${subAgent.SubAgent.Name}" must have at least SubAgentInputMapping or SubAgentOutputMapping`
428
+ );
448
429
  }
430
+ }
431
+
432
+ // Recursively validate nested sub-agents
433
+ if (subAgent.SubAgent.SubAgents && subAgent.SubAgent.SubAgents.length > 0) {
434
+ const nestedValidation = this.validateSubAgents(subAgent.SubAgent.SubAgents);
435
+ if (nestedValidation.errors.length > 0) {
436
+ // Prefix nested errors with parent context
437
+ const prefixedErrors = nestedValidation.errors.map((err) => `SubAgent[${i}] "${subAgent.SubAgent.Name}" -> ${err}`);
438
+ errors.push(...prefixedErrors);
439
+ }
440
+ if (nestedValidation.corrected) {
441
+ corrected = true;
442
+ }
443
+ }
444
+ }
449
445
 
450
- const header = '## AgentSpec Validation Errors\n\nThe generated AgentSpec has the following issues that must be fixed:\n\n';
451
- const errorList = errors.map((err, idx) => `${idx + 1}. ${err}`).join('\n');
452
- const footer = '\n\n**Please fix these issues and regenerate the AgentSpec.**';
446
+ return { errors, corrected };
447
+ }
453
448
 
454
- return header + errorList + footer;
449
+ /**
450
+ * Formats validation errors into a clear, actionable message
451
+ */
452
+ protected formatValidationErrors(errors: string[]): string {
453
+ if (errors.length === 0) {
454
+ return '';
455
455
  }
456
+
457
+ const header = '## AgentSpec Validation Errors\n\nThe generated AgentSpec has the following issues that must be fixed:\n\n';
458
+ const errorList = errors.map((err, idx) => `${idx + 1}. ${err}`).join('\n');
459
+ const footer = '\n\n**Please fix these issues and regenerate the AgentSpec.**';
460
+
461
+ return header + errorList + footer;
462
+ }
456
463
  }