@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -11
- package/dist/agent-spec-sync.d.ts +1 -1
- package/dist/agent-spec-sync.d.ts.map +1 -1
- package/dist/agent-spec-sync.js +76 -83
- package/dist/agent-spec-sync.js.map +1 -1
- package/dist/agents/architect-agent.d.ts.map +1 -1
- package/dist/agents/architect-agent.js +14 -18
- package/dist/agents/architect-agent.js.map +1 -1
- package/dist/agents/builder-agent.d.ts.map +1 -1
- package/dist/agents/builder-agent.js +7 -7
- package/dist/agents/builder-agent.js.map +1 -1
- package/package.json +5 -6
- package/src/agent-spec-sync.ts +1733 -1799
- package/src/agents/architect-agent.ts +385 -378
- package/src/agents/builder-agent.ts +124 -133
package/src/agent-spec-sync.ts
CHANGED
|
@@ -1,46 +1,37 @@
|
|
|
1
|
+
import { Metadata, RunView, UserInfo, LogError } from '@memberjunction/global';
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
AIAgentEntity,
|
|
9
|
-
AIAgentActionEntity,
|
|
10
|
-
AIAgentRelationshipEntity,
|
|
11
|
-
AIAgentStepEntity,
|
|
12
|
-
AIAgentStepPathEntity
|
|
3
|
+
AIAgentEntity,
|
|
4
|
+
AIAgentActionEntity,
|
|
5
|
+
AIAgentRelationshipEntity,
|
|
6
|
+
AIAgentStepEntity,
|
|
7
|
+
AIAgentStepPathEntity,
|
|
13
8
|
} from '@memberjunction/core-entities';
|
|
14
|
-
import {
|
|
15
|
-
AgentSpec,
|
|
16
|
-
AgentActionSpec,
|
|
17
|
-
SubAgentSpec
|
|
18
|
-
} from '@memberjunction/ai-core-plus';
|
|
9
|
+
import { AgentSpec, AgentActionSpec, SubAgentSpec } from '@memberjunction/ai-core-plus';
|
|
19
10
|
|
|
20
11
|
/**
|
|
21
12
|
* Represents a single database mutation performed by AgentSpecSync
|
|
22
13
|
*/
|
|
23
14
|
export interface AgentSpecSyncMutation {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
/** Entity name (e.g., "AI Agents", "AI Prompts", "AIAgentAction") */
|
|
16
|
+
Entity: string;
|
|
17
|
+
/** Operation type */
|
|
18
|
+
Operation: 'Create' | 'Update' | 'Delete';
|
|
19
|
+
/** Record ID */
|
|
20
|
+
ID: string;
|
|
21
|
+
/** Human-readable description of what was done */
|
|
22
|
+
Description: string;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
/**
|
|
35
26
|
* Result of SaveToDatabase operation including all mutations performed
|
|
36
27
|
*/
|
|
37
28
|
export interface AgentSpecSyncResult {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
/** ID of the saved agent */
|
|
30
|
+
agentId: string;
|
|
31
|
+
/** Whether the operation succeeded */
|
|
32
|
+
success: boolean;
|
|
33
|
+
/** Array of all database mutations performed */
|
|
34
|
+
mutations: AgentSpecSyncMutation[];
|
|
44
35
|
}
|
|
45
36
|
|
|
46
37
|
/**
|
|
@@ -49,12 +40,12 @@ export interface AgentSpecSyncResult {
|
|
|
49
40
|
* @private
|
|
50
41
|
*/
|
|
51
42
|
interface DatabaseState {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
actions: AIAgentActionEntity[];
|
|
44
|
+
prompts: any[]; // AIAgentPromptEntity (junction records)
|
|
45
|
+
relationships: AIAgentRelationshipEntity[];
|
|
46
|
+
steps: AIAgentStepEntity[];
|
|
47
|
+
paths: AIAgentStepPathEntity[];
|
|
48
|
+
childAgents: AIAgentEntity[];
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
/**
|
|
@@ -63,12 +54,12 @@ interface DatabaseState {
|
|
|
63
54
|
* @private
|
|
64
55
|
*/
|
|
65
56
|
interface Orphans {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
actions: AIAgentActionEntity[];
|
|
58
|
+
prompts: any[]; // AIAgentPromptEntity junctions (NOT the AIPrompt entities themselves)
|
|
59
|
+
relationships: AIAgentRelationshipEntity[];
|
|
60
|
+
steps: AIAgentStepEntity[];
|
|
61
|
+
paths: AIAgentStepPathEntity[];
|
|
62
|
+
childAgents: AIAgentEntity[]; // Will be orphaned (ParentID = NULL) not deleted
|
|
72
63
|
}
|
|
73
64
|
|
|
74
65
|
/**
|
|
@@ -122,1859 +113,1802 @@ interface Orphans {
|
|
|
122
113
|
* @module @memberjunction/ai-agent-manager
|
|
123
114
|
*/
|
|
124
115
|
export class AgentSpecSync {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
this._contextUser = contextUser;
|
|
116
|
+
/**
|
|
117
|
+
* The raw specification data structure containing all agent configuration
|
|
118
|
+
*/
|
|
119
|
+
public spec: AgentSpec;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Tracks whether this spec has been loaded from the database
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
private _isLoaded: boolean = false;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Tracks whether this spec has unsaved changes
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
private _isDirty: boolean = false;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Context user for database operations (required for server-side operations)
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
private _contextUser?: UserInfo;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Tracks all database mutations performed during save operations
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
private _mutations: AgentSpecSyncMutation[] = [];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a new AgentSpecSync instance.
|
|
147
|
+
*
|
|
148
|
+
* Note: This constructor is typically not called directly. Instead, use the static factory methods:
|
|
149
|
+
* - {@link LoadFromDatabase} - Load existing agent from database
|
|
150
|
+
* - {@link LoadByName} - Load agent by name
|
|
151
|
+
* - {@link FromRawSpec} - Create from raw spec data
|
|
152
|
+
*
|
|
153
|
+
* @param spec - Optional initial spec data (for creating new agents or working with existing data)
|
|
154
|
+
* @param contextUser - Optional context user (required for server-side operations)
|
|
155
|
+
*/
|
|
156
|
+
constructor(spec?: Partial<AgentSpec>, contextUser?: UserInfo) {
|
|
157
|
+
if (spec) {
|
|
158
|
+
this.spec = this.initializeSpec(spec);
|
|
159
|
+
this._isDirty = true;
|
|
160
|
+
} else {
|
|
161
|
+
// Create minimal empty spec
|
|
162
|
+
this.spec = {
|
|
163
|
+
ID: '', // Will be set on save if empty
|
|
164
|
+
Name: '',
|
|
165
|
+
StartingPayloadValidationMode: 'Fail',
|
|
166
|
+
Actions: [],
|
|
167
|
+
SubAgents: [],
|
|
168
|
+
};
|
|
180
169
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
170
|
+
this._contextUser = contextUser;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ===== MUTATION TRACKING METHODS =====
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Track a database mutation performed during save operation
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
private trackMutation(entity: string, operation: 'Create' | 'Update' | 'Delete', id: string, description: string): void {
|
|
180
|
+
this._mutations.push({
|
|
181
|
+
Entity: entity,
|
|
182
|
+
Operation: operation,
|
|
183
|
+
ID: id,
|
|
184
|
+
Description: description,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get all mutations tracked during the last save operation
|
|
190
|
+
* @returns Array of mutations
|
|
191
|
+
*/
|
|
192
|
+
public getMutations(): AgentSpecSyncMutation[] {
|
|
193
|
+
return [...this._mutations];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clear all tracked mutations
|
|
198
|
+
*/
|
|
199
|
+
public clearMutations(): void {
|
|
200
|
+
this._mutations = [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ===== STATIC FACTORY METHODS =====
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Load an agent and its complete hierarchy from the database by ID.
|
|
207
|
+
*
|
|
208
|
+
* This method efficiently loads the agent along with all its actions and sub-agents
|
|
209
|
+
* using batched queries to minimize database round trips. When `includeSubAgents` is true,
|
|
210
|
+
* it recursively loads the entire agent hierarchy.
|
|
211
|
+
*
|
|
212
|
+
* @param agentId - The unique ID of the agent to load
|
|
213
|
+
* @param contextUser - Optional context user (required for server-side operations)
|
|
214
|
+
* @param includeSubAgents - Whether to recursively load all sub-agents (default: true)
|
|
215
|
+
* @returns Promise resolving to AgentSpecSync instance with loaded data
|
|
216
|
+
* @throws {Error} If agent with specified ID is not found
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* // Load agent with all sub-agents
|
|
221
|
+
* const spec = await AgentSpecSync.LoadFromDatabase(
|
|
222
|
+
* 'agent-uuid-here',
|
|
223
|
+
* contextUser,
|
|
224
|
+
* true
|
|
225
|
+
* );
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
static async LoadFromDatabase(agentId: string, contextUser?: UserInfo, includeSubAgents: boolean = true): Promise<AgentSpecSync> {
|
|
229
|
+
const instance = new AgentSpecSync(undefined, contextUser);
|
|
230
|
+
await instance.loadFromEntities(agentId, includeSubAgents);
|
|
231
|
+
return instance;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Load an agent by name (must be unique).
|
|
236
|
+
*
|
|
237
|
+
* Searches for an agent with the specified name and loads it. If multiple agents
|
|
238
|
+
* have the same name, an error is thrown. Agent names should be unique within
|
|
239
|
+
* the system for this method to work reliably.
|
|
240
|
+
*
|
|
241
|
+
* @param agentName - The name of the agent to load
|
|
242
|
+
* @param contextUser - Optional context user (required for server-side operations)
|
|
243
|
+
* @param includeSubAgents - Whether to recursively load sub-agents (default: true)
|
|
244
|
+
* @returns Promise resolving to AgentSpecSync instance with loaded data
|
|
245
|
+
* @throws {Error} If agent is not found or multiple agents have the same name
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* const spec = await AgentSpecSync.LoadByName('My Agent', contextUser);
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
static async LoadByName(agentName: string, contextUser?: UserInfo, includeSubAgents: boolean = true): Promise<AgentSpecSync> {
|
|
253
|
+
// Find agent by name
|
|
254
|
+
const rv = new RunView();
|
|
255
|
+
const result = await rv.RunView<AIAgentEntity>(
|
|
256
|
+
{
|
|
257
|
+
EntityName: 'AI Agents',
|
|
258
|
+
ExtraFilter: `Name='${agentName.replace(/'/g, "''")}'`,
|
|
259
|
+
ResultType: 'entity_object',
|
|
260
|
+
},
|
|
261
|
+
contextUser
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (!result.Success) {
|
|
265
|
+
throw new Error(`Failed to find agent by name: ${result.ErrorMessage}`);
|
|
200
266
|
}
|
|
201
267
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
* @returns Array of mutations
|
|
205
|
-
*/
|
|
206
|
-
public getMutations(): AgentSpecSyncMutation[] {
|
|
207
|
-
return [...this._mutations];
|
|
268
|
+
if (!result.Results || result.Results.length === 0) {
|
|
269
|
+
throw new Error(`Agent with name '${agentName}' not found`);
|
|
208
270
|
}
|
|
209
271
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
*/
|
|
213
|
-
public clearMutations(): void {
|
|
214
|
-
this._mutations = [];
|
|
272
|
+
if (result.Results.length > 1) {
|
|
273
|
+
throw new Error(`Multiple agents found with name '${agentName}'. Use LoadFromDatabase with specific ID instead.`);
|
|
215
274
|
}
|
|
216
275
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
const agent = result.Results[0];
|
|
277
|
+
return AgentSpecSync.LoadFromDatabase(agent.ID, contextUser, includeSubAgents);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Create a new agent spec from a raw specification.
|
|
282
|
+
*
|
|
283
|
+
* This creates an in-memory AgentSpecSync instance from raw data. The agent is not
|
|
284
|
+
* saved to the database until {@link SaveToDatabase} is called.
|
|
285
|
+
*
|
|
286
|
+
* @param rawSpec - The raw spec data conforming to {@link AgentSpec} interface
|
|
287
|
+
* @param contextUser - Optional context user (required when saving server-side)
|
|
288
|
+
* @returns New AgentSpecSync instance (not yet saved to database)
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* const rawSpec: AgentSpec = {
|
|
293
|
+
* ID: '',
|
|
294
|
+
* Name: 'New Agent',
|
|
295
|
+
* Description: 'Agent description',
|
|
296
|
+
* InvocationMode: 'Any',
|
|
297
|
+
* Actions: [],
|
|
298
|
+
* SubAgents: []
|
|
299
|
+
* };
|
|
300
|
+
* const spec = AgentSpecSync.FromRawSpec(rawSpec, contextUser);
|
|
301
|
+
* await spec.SaveToDatabase();
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
static FromRawSpec(rawSpec: AgentSpec, contextUser?: UserInfo): AgentSpecSync {
|
|
305
|
+
return new AgentSpecSync(rawSpec, contextUser);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ===== LOADING METHODS =====
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Load the complete agent specification from database entities.
|
|
312
|
+
*
|
|
313
|
+
* This method orchestrates loading from AIAgent, AIAgentAction, and AIAgentRelationship tables
|
|
314
|
+
* using batched queries for optimal performance. It handles both child agents (ParentID-based)
|
|
315
|
+
* and related agents (relationship-based).
|
|
316
|
+
*
|
|
317
|
+
* @private
|
|
318
|
+
* @param agentId - The agent ID to load
|
|
319
|
+
* @param includeSubAgents - Whether to recursively load sub-agents
|
|
320
|
+
* @throws {Error} If agent is not found
|
|
321
|
+
*/
|
|
322
|
+
private async loadFromEntities(agentId: string, includeSubAgents: boolean): Promise<void> {
|
|
323
|
+
const md = new Metadata();
|
|
324
|
+
const rv = new RunView();
|
|
325
|
+
|
|
326
|
+
// Step 1: Load the main agent entity
|
|
327
|
+
const agentEntity = await md.GetEntityObject<AIAgentEntity>('AI Agents', this._contextUser);
|
|
328
|
+
const loaded = await agentEntity.Load(agentId);
|
|
329
|
+
if (!loaded) {
|
|
330
|
+
throw new Error(`Agent with ID ${agentId} not found`);
|
|
250
331
|
}
|
|
251
332
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const agent = result.Results[0];
|
|
296
|
-
return AgentSpecSync.LoadFromDatabase(agent.ID, contextUser, includeSubAgents);
|
|
333
|
+
// Step 2: Batch load related entities using RunViews for optimal performance
|
|
334
|
+
// Note: Load paths separately after steps to avoid hardcoded view name in subquery
|
|
335
|
+
const [actionsResult, childAgentsResult, relatedAgentsResult, promptsResult, stepsResult] = await rv.RunViews(
|
|
336
|
+
[
|
|
337
|
+
{
|
|
338
|
+
EntityName: 'AI Agent Actions',
|
|
339
|
+
ExtraFilter: `AgentID='${agentId}'`,
|
|
340
|
+
OrderBy: 'Status, ActionID',
|
|
341
|
+
ResultType: 'entity_object',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
EntityName: 'AI Agents',
|
|
345
|
+
ExtraFilter: `ParentID='${agentId}'`,
|
|
346
|
+
OrderBy: 'ExecutionOrder, Name',
|
|
347
|
+
ResultType: 'entity_object',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
EntityName: 'MJ: AI Agent Relationships',
|
|
351
|
+
ExtraFilter: `AgentID='${agentId}' AND Status='Active'`,
|
|
352
|
+
OrderBy: '__mj_CreatedAt',
|
|
353
|
+
ResultType: 'entity_object',
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
EntityName: 'MJ: AI Agent Prompts',
|
|
357
|
+
ExtraFilter: `AgentID='${agentId}'`,
|
|
358
|
+
OrderBy: 'ExecutionOrder',
|
|
359
|
+
ResultType: 'entity_object',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
EntityName: 'MJ: AI Agent Steps',
|
|
363
|
+
ExtraFilter: `AgentID='${agentId}'`,
|
|
364
|
+
OrderBy: 'Name',
|
|
365
|
+
ResultType: 'entity_object',
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
this._contextUser
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Check for errors
|
|
372
|
+
if (!actionsResult.Success) {
|
|
373
|
+
throw new Error(`Failed to load agent actions: ${actionsResult.ErrorMessage}`);
|
|
297
374
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
* Create a new agent spec from a raw specification.
|
|
301
|
-
*
|
|
302
|
-
* This creates an in-memory AgentSpecSync instance from raw data. The agent is not
|
|
303
|
-
* saved to the database until {@link SaveToDatabase} is called.
|
|
304
|
-
*
|
|
305
|
-
* @param rawSpec - The raw spec data conforming to {@link AgentSpec} interface
|
|
306
|
-
* @param contextUser - Optional context user (required when saving server-side)
|
|
307
|
-
* @returns New AgentSpecSync instance (not yet saved to database)
|
|
308
|
-
*
|
|
309
|
-
* @example
|
|
310
|
-
* ```typescript
|
|
311
|
-
* const rawSpec: AgentSpec = {
|
|
312
|
-
* ID: '',
|
|
313
|
-
* Name: 'New Agent',
|
|
314
|
-
* Description: 'Agent description',
|
|
315
|
-
* InvocationMode: 'Any',
|
|
316
|
-
* Actions: [],
|
|
317
|
-
* SubAgents: []
|
|
318
|
-
* };
|
|
319
|
-
* const spec = AgentSpecSync.FromRawSpec(rawSpec, contextUser);
|
|
320
|
-
* await spec.SaveToDatabase();
|
|
321
|
-
* ```
|
|
322
|
-
*/
|
|
323
|
-
static FromRawSpec(rawSpec: AgentSpec, contextUser?: UserInfo): AgentSpecSync {
|
|
324
|
-
return new AgentSpecSync(rawSpec, contextUser);
|
|
375
|
+
if (!childAgentsResult.Success) {
|
|
376
|
+
throw new Error(`Failed to load child agents: ${childAgentsResult.ErrorMessage}`);
|
|
325
377
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Load the complete agent specification from database entities.
|
|
331
|
-
*
|
|
332
|
-
* This method orchestrates loading from AIAgent, AIAgentAction, and AIAgentRelationship tables
|
|
333
|
-
* using batched queries for optimal performance. It handles both child agents (ParentID-based)
|
|
334
|
-
* and related agents (relationship-based).
|
|
335
|
-
*
|
|
336
|
-
* @private
|
|
337
|
-
* @param agentId - The agent ID to load
|
|
338
|
-
* @param includeSubAgents - Whether to recursively load sub-agents
|
|
339
|
-
* @throws {Error} If agent is not found
|
|
340
|
-
*/
|
|
341
|
-
private async loadFromEntities(agentId: string, includeSubAgents: boolean): Promise<void> {
|
|
342
|
-
const md = new Metadata();
|
|
343
|
-
const rv = new RunView();
|
|
344
|
-
|
|
345
|
-
// Step 1: Load the main agent entity
|
|
346
|
-
const agentEntity = await md.GetEntityObject<AIAgentEntity>(
|
|
347
|
-
'AI Agents',
|
|
348
|
-
this._contextUser
|
|
349
|
-
);
|
|
350
|
-
const loaded = await agentEntity.Load(agentId);
|
|
351
|
-
if (!loaded) {
|
|
352
|
-
throw new Error(`Agent with ID ${agentId} not found`);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Step 2: Batch load related entities using RunViews for optimal performance
|
|
356
|
-
// Note: Load paths separately after steps to avoid hardcoded view name in subquery
|
|
357
|
-
const [actionsResult, childAgentsResult, relatedAgentsResult, promptsResult, stepsResult] = await rv.RunViews([
|
|
358
|
-
{
|
|
359
|
-
EntityName: 'AI Agent Actions',
|
|
360
|
-
ExtraFilter: `AgentID='${agentId}'`,
|
|
361
|
-
OrderBy: 'Status, ActionID',
|
|
362
|
-
ResultType: 'entity_object'
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
EntityName: 'AI Agents',
|
|
366
|
-
ExtraFilter: `ParentID='${agentId}'`,
|
|
367
|
-
OrderBy: 'ExecutionOrder, Name',
|
|
368
|
-
ResultType: 'entity_object'
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
EntityName: 'MJ: AI Agent Relationships',
|
|
372
|
-
ExtraFilter: `AgentID='${agentId}' AND Status='Active'`,
|
|
373
|
-
OrderBy: '__mj_CreatedAt',
|
|
374
|
-
ResultType: 'entity_object'
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
EntityName: 'MJ: AI Agent Prompts',
|
|
378
|
-
ExtraFilter: `AgentID='${agentId}'`,
|
|
379
|
-
OrderBy: 'ExecutionOrder',
|
|
380
|
-
ResultType: 'entity_object'
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
EntityName: 'MJ: AI Agent Steps',
|
|
384
|
-
ExtraFilter: `AgentID='${agentId}'`,
|
|
385
|
-
OrderBy: 'Name',
|
|
386
|
-
ResultType: 'entity_object'
|
|
387
|
-
}
|
|
388
|
-
], this._contextUser);
|
|
389
|
-
|
|
390
|
-
// Check for errors
|
|
391
|
-
if (!actionsResult.Success) {
|
|
392
|
-
throw new Error(`Failed to load agent actions: ${actionsResult.ErrorMessage}`);
|
|
393
|
-
}
|
|
394
|
-
if (!childAgentsResult.Success) {
|
|
395
|
-
throw new Error(`Failed to load child agents: ${childAgentsResult.ErrorMessage}`);
|
|
396
|
-
}
|
|
397
|
-
if (!relatedAgentsResult.Success) {
|
|
398
|
-
throw new Error(`Failed to load related agents: ${relatedAgentsResult.ErrorMessage}`);
|
|
399
|
-
}
|
|
400
|
-
if (!promptsResult.Success) {
|
|
401
|
-
throw new Error(`Failed to load agent prompts: ${promptsResult.ErrorMessage}`);
|
|
402
|
-
}
|
|
403
|
-
if (!stepsResult.Success) {
|
|
404
|
-
throw new Error(`Failed to load agent steps: ${stepsResult.ErrorMessage}`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Step 2b: Load paths separately using step IDs to avoid hardcoded view name
|
|
408
|
-
let pathsResult;
|
|
409
|
-
const steps = stepsResult.Results || [];
|
|
410
|
-
if (steps.length > 0) {
|
|
411
|
-
const stepIds = steps.map((s: AIAgentStepEntity) => `'${s.ID}'`).join(',');
|
|
412
|
-
pathsResult = await rv.RunView<AIAgentStepPathEntity>({
|
|
413
|
-
EntityName: 'MJ: AI Agent Step Paths',
|
|
414
|
-
ExtraFilter: `OriginStepID IN (${stepIds})`,
|
|
415
|
-
OrderBy: 'Priority DESC',
|
|
416
|
-
ResultType: 'entity_object'
|
|
417
|
-
}, this._contextUser);
|
|
418
|
-
|
|
419
|
-
if (!pathsResult.Success) {
|
|
420
|
-
throw new Error(`Failed to load step paths: ${pathsResult.ErrorMessage}`);
|
|
421
|
-
}
|
|
422
|
-
} else {
|
|
423
|
-
// No steps, so no paths
|
|
424
|
-
pathsResult = {
|
|
425
|
-
Success: true,
|
|
426
|
-
Results: [],
|
|
427
|
-
RowCount: 0
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Step 2c: Load full AI Prompt records to get PromptText, PromptRole, PromptPosition
|
|
432
|
-
let fullPromptsResult;
|
|
433
|
-
const agentPrompts = promptsResult.Results || [];
|
|
434
|
-
if (agentPrompts.length > 0) {
|
|
435
|
-
const promptIds = agentPrompts.map((p: any) => `'${p.PromptID}'`).join(',');
|
|
436
|
-
fullPromptsResult = await rv.RunView({
|
|
437
|
-
EntityName: 'AI Prompts',
|
|
438
|
-
ExtraFilter: `ID IN (${promptIds})`,
|
|
439
|
-
OrderBy: 'Name',
|
|
440
|
-
ResultType: 'entity_object'
|
|
441
|
-
}, this._contextUser);
|
|
442
|
-
|
|
443
|
-
if (!fullPromptsResult.Success) {
|
|
444
|
-
throw new Error(`Failed to load AI prompts: ${fullPromptsResult.ErrorMessage}`);
|
|
445
|
-
}
|
|
446
|
-
} else {
|
|
447
|
-
// No prompts
|
|
448
|
-
fullPromptsResult = {
|
|
449
|
-
Success: true,
|
|
450
|
-
Results: [],
|
|
451
|
-
RowCount: 0
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Step 3: Map entities to raw spec format
|
|
456
|
-
this.spec = this.mapEntitiesToRawSpec(
|
|
457
|
-
agentEntity,
|
|
458
|
-
actionsResult.Results || [],
|
|
459
|
-
childAgentsResult.Results || [],
|
|
460
|
-
relatedAgentsResult.Results || [],
|
|
461
|
-
promptsResult.Results || [],
|
|
462
|
-
fullPromptsResult.Results || [],
|
|
463
|
-
stepsResult.Results || [],
|
|
464
|
-
pathsResult.Results || []
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
// Step 4: Recursively load sub-agents if requested
|
|
468
|
-
if (includeSubAgents && this.spec.SubAgents && this.spec.SubAgents.length > 0) {
|
|
469
|
-
// Recursively load complete specs for all sub-agents
|
|
470
|
-
for (const subAgentSpec of this.spec.SubAgents) {
|
|
471
|
-
if (subAgentSpec.SubAgent && subAgentSpec.SubAgent.ID) {
|
|
472
|
-
try {
|
|
473
|
-
// Load the complete sub-agent spec recursively
|
|
474
|
-
const subAgentSync = await AgentSpecSync.LoadFromDatabase(
|
|
475
|
-
subAgentSpec.SubAgent.ID,
|
|
476
|
-
this._contextUser,
|
|
477
|
-
true // Recursively load nested sub-agents too
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
// Replace the minimal SubAgent with the complete spec
|
|
481
|
-
const fullSubAgentSpec = subAgentSync.toJSON();
|
|
482
|
-
|
|
483
|
-
// Preserve the relationship metadata while adding full spec details
|
|
484
|
-
subAgentSpec.SubAgent = {
|
|
485
|
-
...fullSubAgentSpec,
|
|
486
|
-
// Ensure we preserve any relationship-specific overrides
|
|
487
|
-
StartingPayloadValidationMode: subAgentSpec.SubAgent.StartingPayloadValidationMode || fullSubAgentSpec.StartingPayloadValidationMode
|
|
488
|
-
};
|
|
489
|
-
} catch (error) {
|
|
490
|
-
LogError(`Failed to load sub-agent ${subAgentSpec.SubAgent.ID}: ${error instanceof Error ? error.message : String(error)}`);
|
|
491
|
-
// Continue with partial data rather than failing completely
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
this._isLoaded = true;
|
|
498
|
-
this._isDirty = false;
|
|
378
|
+
if (!relatedAgentsResult.Success) {
|
|
379
|
+
throw new Error(`Failed to load related agents: ${relatedAgentsResult.ErrorMessage}`);
|
|
499
380
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
* Map database entities to AgentSpec format.
|
|
503
|
-
*
|
|
504
|
-
* Transforms the normalized database entities into a single denormalized specification
|
|
505
|
-
* object that's easy to work with in code. Handles JSON parsing for all structured fields.
|
|
506
|
-
*
|
|
507
|
-
* @private
|
|
508
|
-
* @param agent - The main agent entity
|
|
509
|
-
* @param actions - Array of agent action entities
|
|
510
|
-
* @param childAgents - Array of child agent entities (ParentID-based)
|
|
511
|
-
* @param relatedAgents - Array of related agent relationship entities
|
|
512
|
-
* @param agentPrompts - Array of agent prompt junction entities
|
|
513
|
-
* @param fullPrompts - Array of full AI Prompt entities with PromptText, PromptRole, PromptPosition
|
|
514
|
-
* @param steps - Array of agent step entities (for Flow agents)
|
|
515
|
-
* @param paths - Array of step path entities (for Flow agents)
|
|
516
|
-
* @returns Fully populated AgentSpec object
|
|
517
|
-
*/
|
|
518
|
-
private mapEntitiesToRawSpec(
|
|
519
|
-
agent: AIAgentEntity,
|
|
520
|
-
actions: AIAgentActionEntity[],
|
|
521
|
-
childAgents: AIAgentEntity[],
|
|
522
|
-
relatedAgents: AIAgentRelationshipEntity[],
|
|
523
|
-
agentPrompts: any[],
|
|
524
|
-
fullPrompts: any[],
|
|
525
|
-
steps: AIAgentStepEntity[],
|
|
526
|
-
paths: AIAgentStepPathEntity[]
|
|
527
|
-
): AgentSpec {
|
|
528
|
-
// Map all agent fields to spec
|
|
529
|
-
const spec: AgentSpec = {
|
|
530
|
-
ID: agent.ID,
|
|
531
|
-
Name: agent.Name || '',
|
|
532
|
-
Description: agent.Description || undefined,
|
|
533
|
-
TypeID: agent.TypeID || undefined,
|
|
534
|
-
Status: (agent.Status as 'Active' | 'Inactive' | 'Pending') || undefined,
|
|
535
|
-
IconClass: agent.IconClass || undefined,
|
|
536
|
-
LogoURL: agent.LogoURL || undefined,
|
|
537
|
-
ParentID: agent.ParentID || undefined,
|
|
538
|
-
DriverClass: agent.DriverClass || undefined,
|
|
539
|
-
ModelSelectionMode: agent.ModelSelectionMode,
|
|
540
|
-
|
|
541
|
-
// Parse JSON array fields
|
|
542
|
-
PayloadDownstreamPaths: this.parseJsonField<string[]>(agent.PayloadDownstreamPaths),
|
|
543
|
-
PayloadUpstreamPaths: this.parseJsonField<string[]>(agent.PayloadUpstreamPaths),
|
|
544
|
-
PayloadSelfReadPaths: this.parseJsonField<string[]>(agent.PayloadSelfReadPaths),
|
|
545
|
-
PayloadSelfWritePaths: this.parseJsonField<string[]>(agent.PayloadSelfWritePaths),
|
|
546
|
-
PayloadScope: agent.PayloadScope || undefined,
|
|
547
|
-
|
|
548
|
-
// Validation fields
|
|
549
|
-
FinalPayloadValidation: agent.FinalPayloadValidation || null,
|
|
550
|
-
FinalPayloadValidationMode: agent.FinalPayloadValidationMode,
|
|
551
|
-
FinalPayloadValidationMaxRetries: agent.FinalPayloadValidationMaxRetries || undefined,
|
|
552
|
-
|
|
553
|
-
StartingPayloadValidation: agent.StartingPayloadValidation || null,
|
|
554
|
-
StartingPayloadValidationMode: agent.StartingPayloadValidationMode as any,
|
|
555
|
-
|
|
556
|
-
// Resource limits
|
|
557
|
-
MaxCostPerRun: agent.MaxCostPerRun || null,
|
|
558
|
-
MaxTokensPerRun: agent.MaxTokensPerRun || null,
|
|
559
|
-
MaxIterationsPerRun: agent.MaxIterationsPerRun || null,
|
|
560
|
-
MaxTimePerRun: agent.MaxTimePerRun || undefined,
|
|
561
|
-
|
|
562
|
-
// Execution frequency
|
|
563
|
-
MinExecutionsPerRun: agent.MinExecutionsPerRun || undefined,
|
|
564
|
-
MaxExecutionsPerRun: agent.MaxExecutionsPerRun || undefined,
|
|
565
|
-
|
|
566
|
-
// Other config
|
|
567
|
-
DefaultPromptEffortLevel: agent.DefaultPromptEffortLevel || undefined,
|
|
568
|
-
ChatHandlingOption: agent.ChatHandlingOption || undefined,
|
|
569
|
-
DefaultArtifactTypeID: agent.DefaultArtifactTypeID || undefined,
|
|
570
|
-
OwnerUserID: agent.OwnerUserID || undefined,
|
|
571
|
-
InvocationMode: agent.InvocationMode,
|
|
572
|
-
|
|
573
|
-
// Requirements and design documentation
|
|
574
|
-
FunctionalRequirements: (agent as any).FunctionalRequirements || null,
|
|
575
|
-
TechnicalDesign: (agent as any).TechnicalDesign || null,
|
|
576
|
-
|
|
577
|
-
// Map actions
|
|
578
|
-
Actions: actions.map(action => this.mapActionEntityToSpec(action)),
|
|
579
|
-
|
|
580
|
-
// Map sub-agents (both child and related)
|
|
581
|
-
SubAgents: [
|
|
582
|
-
...childAgents.map(child => this.mapChildAgentToSpec(child)),
|
|
583
|
-
...relatedAgents.map(rel => this.mapRelatedAgentToSpec(rel))
|
|
584
|
-
],
|
|
585
|
-
|
|
586
|
-
// Map prompts (agent-level prompts for Loop agents)
|
|
587
|
-
Prompts: agentPrompts.map(agentPrompt => this.mapPromptEntityToSpec(agentPrompt, fullPrompts)),
|
|
588
|
-
|
|
589
|
-
// Map steps (for Flow agents)
|
|
590
|
-
Steps: steps.map(step => this.mapStepEntityToSpec(step)),
|
|
591
|
-
|
|
592
|
-
// Map paths (for Flow agents)
|
|
593
|
-
Paths: paths.map(path => this.mapPathEntityToSpec(path))
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
return spec;
|
|
381
|
+
if (!promptsResult.Success) {
|
|
382
|
+
throw new Error(`Failed to load agent prompts: ${promptsResult.ErrorMessage}`);
|
|
597
383
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
* Map AIAgentActionEntity to AgentActionSpec format.
|
|
601
|
-
*
|
|
602
|
-
* @private
|
|
603
|
-
* @param action - The agent action entity from the database
|
|
604
|
-
* @returns Mapped action spec
|
|
605
|
-
*/
|
|
606
|
-
private mapActionEntityToSpec(action: AIAgentActionEntity): AgentActionSpec {
|
|
607
|
-
return {
|
|
608
|
-
AgentActionID: action.ID,
|
|
609
|
-
ActionID: action.ActionID || '',
|
|
610
|
-
Status: action.Status,
|
|
611
|
-
MaxExecutionsPerRun: action.MaxExecutionsPerRun || undefined,
|
|
612
|
-
ResultExpirationTurns: action.ResultExpirationTurns || undefined,
|
|
613
|
-
ResultExpirationMode: action.ResultExpirationMode || undefined,
|
|
614
|
-
CompactMode: action.CompactMode || undefined,
|
|
615
|
-
CompactLength: action.CompactLength || undefined,
|
|
616
|
-
CompactPromptID: action.CompactPromptID || null
|
|
617
|
-
};
|
|
384
|
+
if (!stepsResult.Success) {
|
|
385
|
+
throw new Error(`Failed to load agent steps: ${stepsResult.ErrorMessage}`);
|
|
618
386
|
}
|
|
619
387
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
388
|
+
// Step 2b: Load paths separately using step IDs to avoid hardcoded view name
|
|
389
|
+
let pathsResult;
|
|
390
|
+
const steps = stepsResult.Results || [];
|
|
391
|
+
if (steps.length > 0) {
|
|
392
|
+
const stepIds = steps.map((s: AIAgentStepEntity) => `'${s.ID}'`).join(',');
|
|
393
|
+
pathsResult = await rv.RunView<AIAgentStepPathEntity>(
|
|
394
|
+
{
|
|
395
|
+
EntityName: 'MJ: AI Agent Step Paths',
|
|
396
|
+
ExtraFilter: `OriginStepID IN (${stepIds})`,
|
|
397
|
+
OrderBy: 'Priority DESC',
|
|
398
|
+
ResultType: 'entity_object',
|
|
399
|
+
},
|
|
400
|
+
this._contextUser
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (!pathsResult.Success) {
|
|
404
|
+
throw new Error(`Failed to load step paths: ${pathsResult.ErrorMessage}`);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
// No steps, so no paths
|
|
408
|
+
pathsResult = {
|
|
409
|
+
Success: true,
|
|
410
|
+
Results: [],
|
|
411
|
+
RowCount: 0,
|
|
412
|
+
};
|
|
636
413
|
}
|
|
637
414
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
415
|
+
// Step 2c: Load full AI Prompt records to get PromptText, PromptRole, PromptPosition
|
|
416
|
+
let fullPromptsResult;
|
|
417
|
+
const agentPrompts = promptsResult.Results || [];
|
|
418
|
+
if (agentPrompts.length > 0) {
|
|
419
|
+
const promptIds = agentPrompts.map((p: any) => `'${p.PromptID}'`).join(',');
|
|
420
|
+
fullPromptsResult = await rv.RunView(
|
|
421
|
+
{
|
|
422
|
+
EntityName: 'AI Prompts',
|
|
423
|
+
ExtraFilter: `ID IN (${promptIds})`,
|
|
424
|
+
OrderBy: 'Name',
|
|
425
|
+
ResultType: 'entity_object',
|
|
426
|
+
},
|
|
427
|
+
this._contextUser
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
if (!fullPromptsResult.Success) {
|
|
431
|
+
throw new Error(`Failed to load AI prompts: ${fullPromptsResult.ErrorMessage}`);
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
// No prompts
|
|
435
|
+
fullPromptsResult = {
|
|
436
|
+
Success: true,
|
|
437
|
+
Results: [],
|
|
438
|
+
RowCount: 0,
|
|
439
|
+
};
|
|
658
440
|
}
|
|
659
441
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
442
|
+
// Step 3: Map entities to raw spec format
|
|
443
|
+
this.spec = this.mapEntitiesToRawSpec(
|
|
444
|
+
agentEntity,
|
|
445
|
+
actionsResult.Results || [],
|
|
446
|
+
childAgentsResult.Results || [],
|
|
447
|
+
relatedAgentsResult.Results || [],
|
|
448
|
+
promptsResult.Results || [],
|
|
449
|
+
fullPromptsResult.Results || [],
|
|
450
|
+
stepsResult.Results || [],
|
|
451
|
+
pathsResult.Results || []
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Step 4: Recursively load sub-agents if requested
|
|
455
|
+
if (includeSubAgents && this.spec.SubAgents && this.spec.SubAgents.length > 0) {
|
|
456
|
+
// Recursively load complete specs for all sub-agents
|
|
457
|
+
for (const subAgentSpec of this.spec.SubAgents) {
|
|
458
|
+
if (subAgentSpec.SubAgent && subAgentSpec.SubAgent.ID) {
|
|
459
|
+
try {
|
|
460
|
+
// Load the complete sub-agent spec recursively
|
|
461
|
+
const subAgentSync = await AgentSpecSync.LoadFromDatabase(
|
|
462
|
+
subAgentSpec.SubAgent.ID,
|
|
463
|
+
this._contextUser,
|
|
464
|
+
true // Recursively load nested sub-agents too
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// Replace the minimal SubAgent with the complete spec
|
|
468
|
+
const fullSubAgentSpec = subAgentSync.toJSON();
|
|
469
|
+
|
|
470
|
+
// Preserve the relationship metadata while adding full spec details
|
|
471
|
+
subAgentSpec.SubAgent = {
|
|
472
|
+
...fullSubAgentSpec,
|
|
473
|
+
// Ensure we preserve any relationship-specific overrides
|
|
474
|
+
StartingPayloadValidationMode:
|
|
475
|
+
subAgentSpec.SubAgent.StartingPayloadValidationMode || fullSubAgentSpec.StartingPayloadValidationMode,
|
|
680
476
|
};
|
|
477
|
+
} catch (error) {
|
|
478
|
+
LogError(`Failed to load sub-agent ${subAgentSpec.SubAgent.ID}: ${error instanceof Error ? error.message : String(error)}`);
|
|
479
|
+
// Continue with partial data rather than failing completely
|
|
480
|
+
}
|
|
681
481
|
}
|
|
682
|
-
|
|
683
|
-
return {
|
|
684
|
-
ID: agentPrompt.ID, // Junction record ID for orphan detection
|
|
685
|
-
PromptID: fullPrompt.ID || '',
|
|
686
|
-
PromptText: fullPrompt.TemplateText || '',
|
|
687
|
-
PromptRole: fullPrompt.PromptRole || 'System',
|
|
688
|
-
PromptPosition: fullPrompt.PromptPosition || 'First'
|
|
689
|
-
};
|
|
482
|
+
}
|
|
690
483
|
}
|
|
691
484
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
485
|
+
this._isLoaded = true;
|
|
486
|
+
this._isDirty = false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Map database entities to AgentSpec format.
|
|
491
|
+
*
|
|
492
|
+
* Transforms the normalized database entities into a single denormalized specification
|
|
493
|
+
* object that's easy to work with in code. Handles JSON parsing for all structured fields.
|
|
494
|
+
*
|
|
495
|
+
* @private
|
|
496
|
+
* @param agent - The main agent entity
|
|
497
|
+
* @param actions - Array of agent action entities
|
|
498
|
+
* @param childAgents - Array of child agent entities (ParentID-based)
|
|
499
|
+
* @param relatedAgents - Array of related agent relationship entities
|
|
500
|
+
* @param agentPrompts - Array of agent prompt junction entities
|
|
501
|
+
* @param fullPrompts - Array of full AI Prompt entities with PromptText, PromptRole, PromptPosition
|
|
502
|
+
* @param steps - Array of agent step entities (for Flow agents)
|
|
503
|
+
* @param paths - Array of step path entities (for Flow agents)
|
|
504
|
+
* @returns Fully populated AgentSpec object
|
|
505
|
+
*/
|
|
506
|
+
private mapEntitiesToRawSpec(
|
|
507
|
+
agent: AIAgentEntity,
|
|
508
|
+
actions: AIAgentActionEntity[],
|
|
509
|
+
childAgents: AIAgentEntity[],
|
|
510
|
+
relatedAgents: AIAgentRelationshipEntity[],
|
|
511
|
+
agentPrompts: any[],
|
|
512
|
+
fullPrompts: any[],
|
|
513
|
+
steps: AIAgentStepEntity[],
|
|
514
|
+
paths: AIAgentStepPathEntity[]
|
|
515
|
+
): AgentSpec {
|
|
516
|
+
// Map all agent fields to spec
|
|
517
|
+
const spec: AgentSpec = {
|
|
518
|
+
ID: agent.ID,
|
|
519
|
+
Name: agent.Name || '',
|
|
520
|
+
Description: agent.Description || undefined,
|
|
521
|
+
TypeID: agent.TypeID || undefined,
|
|
522
|
+
Status: (agent.Status as 'Active' | 'Inactive' | 'Pending') || undefined,
|
|
523
|
+
IconClass: agent.IconClass || undefined,
|
|
524
|
+
LogoURL: agent.LogoURL || undefined,
|
|
525
|
+
ParentID: agent.ParentID || undefined,
|
|
526
|
+
DriverClass: agent.DriverClass || undefined,
|
|
527
|
+
ModelSelectionMode: agent.ModelSelectionMode,
|
|
528
|
+
|
|
529
|
+
// Parse JSON array fields
|
|
530
|
+
PayloadDownstreamPaths: this.parseJsonField<string[]>(agent.PayloadDownstreamPaths),
|
|
531
|
+
PayloadUpstreamPaths: this.parseJsonField<string[]>(agent.PayloadUpstreamPaths),
|
|
532
|
+
PayloadSelfReadPaths: this.parseJsonField<string[]>(agent.PayloadSelfReadPaths),
|
|
533
|
+
PayloadSelfWritePaths: this.parseJsonField<string[]>(agent.PayloadSelfWritePaths),
|
|
534
|
+
PayloadScope: agent.PayloadScope || undefined,
|
|
535
|
+
|
|
536
|
+
// Validation fields
|
|
537
|
+
FinalPayloadValidation: agent.FinalPayloadValidation || null,
|
|
538
|
+
FinalPayloadValidationMode: agent.FinalPayloadValidationMode,
|
|
539
|
+
FinalPayloadValidationMaxRetries: agent.FinalPayloadValidationMaxRetries || undefined,
|
|
540
|
+
|
|
541
|
+
StartingPayloadValidation: agent.StartingPayloadValidation || null,
|
|
542
|
+
StartingPayloadValidationMode: agent.StartingPayloadValidationMode as any,
|
|
543
|
+
|
|
544
|
+
// Resource limits
|
|
545
|
+
MaxCostPerRun: agent.MaxCostPerRun || null,
|
|
546
|
+
MaxTokensPerRun: agent.MaxTokensPerRun || null,
|
|
547
|
+
MaxIterationsPerRun: agent.MaxIterationsPerRun || null,
|
|
548
|
+
MaxTimePerRun: agent.MaxTimePerRun || undefined,
|
|
549
|
+
|
|
550
|
+
// Execution frequency
|
|
551
|
+
MinExecutionsPerRun: agent.MinExecutionsPerRun || undefined,
|
|
552
|
+
MaxExecutionsPerRun: agent.MaxExecutionsPerRun || undefined,
|
|
553
|
+
|
|
554
|
+
// Other config
|
|
555
|
+
DefaultPromptEffortLevel: agent.DefaultPromptEffortLevel || undefined,
|
|
556
|
+
ChatHandlingOption: agent.ChatHandlingOption || undefined,
|
|
557
|
+
DefaultArtifactTypeID: agent.DefaultArtifactTypeID || undefined,
|
|
558
|
+
OwnerUserID: agent.OwnerUserID || undefined,
|
|
559
|
+
InvocationMode: agent.InvocationMode,
|
|
560
|
+
|
|
561
|
+
// Requirements and design documentation
|
|
562
|
+
FunctionalRequirements: (agent as any).FunctionalRequirements || null,
|
|
563
|
+
TechnicalDesign: (agent as any).TechnicalDesign || null,
|
|
564
|
+
|
|
565
|
+
// Map actions
|
|
566
|
+
Actions: actions.map((action) => this.mapActionEntityToSpec(action)),
|
|
567
|
+
|
|
568
|
+
// Map sub-agents (both child and related)
|
|
569
|
+
SubAgents: [
|
|
570
|
+
...childAgents.map((child) => this.mapChildAgentToSpec(child)),
|
|
571
|
+
...relatedAgents.map((rel) => this.mapRelatedAgentToSpec(rel)),
|
|
572
|
+
],
|
|
573
|
+
|
|
574
|
+
// Map prompts (agent-level prompts for Loop agents)
|
|
575
|
+
Prompts: agentPrompts.map((agentPrompt) => this.mapPromptEntityToSpec(agentPrompt, fullPrompts)),
|
|
576
|
+
|
|
577
|
+
// Map steps (for Flow agents)
|
|
578
|
+
Steps: steps.map((step) => this.mapStepEntityToSpec(step)),
|
|
579
|
+
|
|
580
|
+
// Map paths (for Flow agents)
|
|
581
|
+
Paths: paths.map((path) => this.mapPathEntityToSpec(path)),
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
return spec;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Map AIAgentActionEntity to AgentActionSpec format.
|
|
589
|
+
*
|
|
590
|
+
* @private
|
|
591
|
+
* @param action - The agent action entity from the database
|
|
592
|
+
* @returns Mapped action spec
|
|
593
|
+
*/
|
|
594
|
+
private mapActionEntityToSpec(action: AIAgentActionEntity): AgentActionSpec {
|
|
595
|
+
return {
|
|
596
|
+
AgentActionID: action.ID,
|
|
597
|
+
ActionID: action.ActionID || '',
|
|
598
|
+
Status: action.Status,
|
|
599
|
+
MaxExecutionsPerRun: action.MaxExecutionsPerRun || undefined,
|
|
600
|
+
ResultExpirationTurns: action.ResultExpirationTurns || undefined,
|
|
601
|
+
ResultExpirationMode: action.ResultExpirationMode || undefined,
|
|
602
|
+
CompactMode: action.CompactMode || undefined,
|
|
603
|
+
CompactLength: action.CompactLength || undefined,
|
|
604
|
+
CompactPromptID: action.CompactPromptID || null,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Map child agent (ParentID-based) to SubAgentSpec format.
|
|
610
|
+
*
|
|
611
|
+
* @private
|
|
612
|
+
* @param childAgent - The child agent entity
|
|
613
|
+
* @returns Mapped sub-agent spec
|
|
614
|
+
*/
|
|
615
|
+
private mapChildAgentToSpec(childAgent: AIAgentEntity): SubAgentSpec {
|
|
616
|
+
return {
|
|
617
|
+
Type: 'child',
|
|
618
|
+
SubAgent: {
|
|
619
|
+
ID: childAgent.ID,
|
|
620
|
+
Name: childAgent.Name || '',
|
|
621
|
+
StartingPayloadValidationMode: 'Fail',
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Map related agent (relationship-based) to SubAgentSpec format.
|
|
628
|
+
*
|
|
629
|
+
* @private
|
|
630
|
+
* @param relationship - The agent relationship entity
|
|
631
|
+
* @returns Mapped sub-agent spec
|
|
632
|
+
*/
|
|
633
|
+
private mapRelatedAgentToSpec(relationship: AIAgentRelationshipEntity): SubAgentSpec {
|
|
634
|
+
return {
|
|
635
|
+
Type: 'related',
|
|
636
|
+
SubAgent: {
|
|
637
|
+
ID: relationship.SubAgentID,
|
|
638
|
+
Name: relationship.SubAgent || '',
|
|
639
|
+
StartingPayloadValidationMode: 'Fail',
|
|
640
|
+
},
|
|
641
|
+
AgentRelationshipID: relationship.ID,
|
|
642
|
+
SubAgentInputMapping: this.parseJsonField<Record<string, string>>(relationship.SubAgentInputMapping),
|
|
643
|
+
SubAgentOutputMapping: this.parseJsonField<Record<string, string>>(relationship.SubAgentOutputMapping),
|
|
644
|
+
SubAgentContextPaths: this.parseJsonField<Record<string, string>>(relationship.SubAgentContextPaths),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Map AIAgentPromptEntity junction to AgentPromptSpec format.
|
|
650
|
+
*
|
|
651
|
+
* @private
|
|
652
|
+
* @param agentPrompt - The agent prompt junction entity (has PromptID, ExecutionOrder)
|
|
653
|
+
* @param fullPrompts - Array of full AI Prompt entities
|
|
654
|
+
* @returns Mapped prompt spec
|
|
655
|
+
*/
|
|
656
|
+
private mapPromptEntityToSpec(agentPrompt: any, fullPrompts: any[]): any {
|
|
657
|
+
// Find the full prompt data by matching PromptID
|
|
658
|
+
const fullPrompt = fullPrompts.find((p: any) => p.ID === agentPrompt.PromptID);
|
|
659
|
+
|
|
660
|
+
if (!fullPrompt) {
|
|
661
|
+
// Fallback if prompt not found
|
|
662
|
+
return {
|
|
663
|
+
ID: agentPrompt.ID, // Junction record ID for orphan detection
|
|
664
|
+
PromptID: agentPrompt.PromptID || '',
|
|
665
|
+
PromptText: '',
|
|
666
|
+
PromptRole: 'System',
|
|
667
|
+
PromptPosition: 'First',
|
|
668
|
+
};
|
|
712
669
|
}
|
|
713
670
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
671
|
+
return {
|
|
672
|
+
ID: agentPrompt.ID, // Junction record ID for orphan detection
|
|
673
|
+
PromptID: fullPrompt.ID || '',
|
|
674
|
+
PromptText: fullPrompt.TemplateText || '',
|
|
675
|
+
PromptRole: fullPrompt.PromptRole || 'System',
|
|
676
|
+
PromptPosition: fullPrompt.PromptPosition || 'First',
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Map AIAgentStepEntity to AgentStep format.
|
|
682
|
+
*
|
|
683
|
+
* @private
|
|
684
|
+
* @param step - The agent step entity
|
|
685
|
+
* @returns Mapped step spec
|
|
686
|
+
*/
|
|
687
|
+
private mapStepEntityToSpec(step: AIAgentStepEntity): any {
|
|
688
|
+
return {
|
|
689
|
+
ID: step.ID,
|
|
690
|
+
Name: step.Name,
|
|
691
|
+
Description: step.Description || undefined,
|
|
692
|
+
StepType: step.StepType,
|
|
693
|
+
StartingStep: step.StartingStep,
|
|
694
|
+
ActionID: step.ActionID || undefined,
|
|
695
|
+
SubAgentID: step.SubAgentID || undefined,
|
|
696
|
+
PromptID: step.PromptID || undefined,
|
|
697
|
+
ActionInputMapping: this.parseJsonField<any>(step.ActionInputMapping),
|
|
698
|
+
ActionOutputMapping: this.parseJsonField<any>(step.ActionOutputMapping),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Map AIAgentStepPathEntity to AgentStepPath format.
|
|
704
|
+
*
|
|
705
|
+
* @private
|
|
706
|
+
* @param path - The agent step path entity
|
|
707
|
+
* @returns Mapped path spec
|
|
708
|
+
*/
|
|
709
|
+
private mapPathEntityToSpec(path: AIAgentStepPathEntity): any {
|
|
710
|
+
return {
|
|
711
|
+
ID: path.ID,
|
|
712
|
+
OriginStepID: path.OriginStepID,
|
|
713
|
+
DestinationStepID: path.DestinationStepID,
|
|
714
|
+
Condition: path.Condition || undefined,
|
|
715
|
+
Description: path.Description || undefined,
|
|
716
|
+
Priority: path.Priority,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ===== SAVING METHODS =====
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Save the current spec to the database.
|
|
724
|
+
*
|
|
725
|
+
* This will create new records or update existing ones based on whether IDs exist.
|
|
726
|
+
* The save operation is performed atomically - if any part fails, no changes are committed.
|
|
727
|
+
*
|
|
728
|
+
* Validation is performed before saving. All entity-level validations defined in the
|
|
729
|
+
* database schema are executed, and any failures will prevent the save.
|
|
730
|
+
*
|
|
731
|
+
* @param validate - Whether to validate before saving (default: true)
|
|
732
|
+
* @returns Promise resolving to result with agent ID, success flag, and all mutations performed
|
|
733
|
+
* @throws {Error} If validation fails or save operation fails
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* ```typescript
|
|
737
|
+
* const spec = new AgentSpecSync({
|
|
738
|
+
* Name: 'New Agent',
|
|
739
|
+
* InvocationMode: 'Any'
|
|
740
|
+
* }, contextUser);
|
|
741
|
+
*
|
|
742
|
+
* const result = await spec.SaveToDatabase();
|
|
743
|
+
* console.log('Saved with ID:', result.agentId);
|
|
744
|
+
* console.log('Mutations:', result.mutations.length);
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
747
|
+
async SaveToDatabase(validate: boolean = true): Promise<AgentSpecSyncResult> {
|
|
748
|
+
// Clear previous mutations
|
|
749
|
+
this.clearMutations();
|
|
750
|
+
|
|
751
|
+
if (!this._isDirty && this._isLoaded) {
|
|
752
|
+
// No changes to save
|
|
753
|
+
return {
|
|
754
|
+
agentId: this.spec.ID,
|
|
755
|
+
success: true,
|
|
756
|
+
mutations: [],
|
|
757
|
+
};
|
|
730
758
|
}
|
|
731
759
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
* This will create new records or update existing ones based on whether IDs exist.
|
|
738
|
-
* The save operation is performed atomically - if any part fails, no changes are committed.
|
|
739
|
-
*
|
|
740
|
-
* Validation is performed before saving. All entity-level validations defined in the
|
|
741
|
-
* database schema are executed, and any failures will prevent the save.
|
|
742
|
-
*
|
|
743
|
-
* @param validate - Whether to validate before saving (default: true)
|
|
744
|
-
* @returns Promise resolving to result with agent ID, success flag, and all mutations performed
|
|
745
|
-
* @throws {Error} If validation fails or save operation fails
|
|
746
|
-
*
|
|
747
|
-
* @example
|
|
748
|
-
* ```typescript
|
|
749
|
-
* const spec = new AgentSpecSync({
|
|
750
|
-
* Name: 'New Agent',
|
|
751
|
-
* InvocationMode: 'Any'
|
|
752
|
-
* }, contextUser);
|
|
753
|
-
*
|
|
754
|
-
* const result = await spec.SaveToDatabase();
|
|
755
|
-
* console.log('Saved with ID:', result.agentId);
|
|
756
|
-
* console.log('Mutations:', result.mutations.length);
|
|
757
|
-
* ```
|
|
758
|
-
*/
|
|
759
|
-
async SaveToDatabase(validate: boolean = true): Promise<AgentSpecSyncResult> {
|
|
760
|
-
// Clear previous mutations
|
|
761
|
-
this.clearMutations();
|
|
762
|
-
|
|
763
|
-
if (!this._isDirty && this._isLoaded) {
|
|
764
|
-
// No changes to save
|
|
765
|
-
return {
|
|
766
|
-
agentId: this.spec.ID,
|
|
767
|
-
success: true,
|
|
768
|
-
mutations: []
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
try {
|
|
773
|
-
// Step 0: Delete orphaned records (if this is an update)
|
|
774
|
-
if (this._isLoaded && this.spec.ID) {
|
|
775
|
-
console.log(`🗑️ SaveToDatabase: Loading current database state for agent ${this.spec.ID}...`);
|
|
776
|
-
const dbState = await this.loadCurrentDatabaseState(this.spec.ID);
|
|
777
|
-
|
|
778
|
-
console.log(`🔍 SaveToDatabase: Identifying orphaned records...`);
|
|
779
|
-
const orphans = this.identifyOrphans(dbState, this.spec);
|
|
780
|
-
|
|
781
|
-
console.log(`🗑️ SaveToDatabase: Deleting ${orphans.paths.length} paths, ${orphans.actions.length} actions, ${orphans.steps.length} steps, ${orphans.prompts.length} prompts, ${orphans.relationships.length} relationships, orphaning ${orphans.childAgents.length} child agents...`);
|
|
782
|
-
await this.deleteOrphans(orphans);
|
|
783
|
-
|
|
784
|
-
console.log(`✅ SaveToDatabase: Orphan cleanup complete`);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Step 1: Save main agent entity
|
|
788
|
-
const agentId = await this.saveAgentEntity(validate);
|
|
789
|
-
|
|
790
|
-
// Step 2: Save actions
|
|
791
|
-
await this.saveActions(agentId);
|
|
792
|
-
|
|
793
|
-
// Step 3: Save sub-agents (both child and related)
|
|
794
|
-
await this.saveSubAgents(agentId);
|
|
795
|
-
|
|
796
|
-
// Step 4: Save prompts
|
|
797
|
-
await this.savePrompts(agentId);
|
|
798
|
-
|
|
799
|
-
// Step 5: Save steps (Flow agents only)
|
|
800
|
-
await this.saveSteps(agentId);
|
|
801
|
-
|
|
802
|
-
// Step 6: Save step paths (Flow agents only)
|
|
803
|
-
await this.saveStepPaths(agentId);
|
|
760
|
+
try {
|
|
761
|
+
// Step 0: Delete orphaned records (if this is an update)
|
|
762
|
+
if (this._isLoaded && this.spec.ID) {
|
|
763
|
+
console.log(`🗑️ SaveToDatabase: Loading current database state for agent ${this.spec.ID}...`);
|
|
764
|
+
const dbState = await this.loadCurrentDatabaseState(this.spec.ID);
|
|
804
765
|
|
|
805
|
-
|
|
806
|
-
|
|
766
|
+
console.log(`🔍 SaveToDatabase: Identifying orphaned records...`);
|
|
767
|
+
const orphans = this.identifyOrphans(dbState, this.spec);
|
|
807
768
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
success: true,
|
|
811
|
-
mutations: this.getMutations()
|
|
812
|
-
};
|
|
813
|
-
} catch (error) {
|
|
814
|
-
// Return failure with whatever mutations were completed before error
|
|
815
|
-
return {
|
|
816
|
-
agentId: this.spec.ID || '',
|
|
817
|
-
success: false,
|
|
818
|
-
mutations: this.getMutations()
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/**
|
|
824
|
-
* Save the main AIAgent entity.
|
|
825
|
-
*
|
|
826
|
-
* @private
|
|
827
|
-
* @param validate - Whether to perform validation before saving
|
|
828
|
-
* @returns Promise resolving to the saved agent ID
|
|
829
|
-
* @throws {Error} If validation fails or save fails
|
|
830
|
-
*/
|
|
831
|
-
private async saveAgentEntity(validate: boolean): Promise<string> {
|
|
832
|
-
const md = new Metadata();
|
|
833
|
-
const agentEntity = await md.GetEntityObject<AIAgentEntity>(
|
|
834
|
-
'AI Agents',
|
|
835
|
-
this._contextUser
|
|
769
|
+
console.log(
|
|
770
|
+
`🗑️ SaveToDatabase: Deleting ${orphans.paths.length} paths, ${orphans.actions.length} actions, ${orphans.steps.length} steps, ${orphans.prompts.length} prompts, ${orphans.relationships.length} relationships, orphaning ${orphans.childAgents.length} child agents...`
|
|
836
771
|
);
|
|
772
|
+
await this.deleteOrphans(orphans);
|
|
837
773
|
|
|
838
|
-
|
|
839
|
-
|
|
774
|
+
console.log(`✅ SaveToDatabase: Orphan cleanup complete`);
|
|
775
|
+
}
|
|
840
776
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const loaded = await agentEntity.Load(this.spec.ID);
|
|
844
|
-
if (!loaded) {
|
|
845
|
-
throw new Error(`Cannot update non-existent agent with ID ${this.spec.ID}`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
777
|
+
// Step 1: Save main agent entity
|
|
778
|
+
const agentId = await this.saveAgentEntity(validate);
|
|
848
779
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
agentEntity.Description = this.spec.Description || null;
|
|
852
|
-
agentEntity.IconClass = this.spec.IconClass || null;
|
|
853
|
-
agentEntity.LogoURL = this.spec.LogoURL || null;
|
|
854
|
-
agentEntity.ParentID = this.spec.ParentID || null;
|
|
855
|
-
agentEntity.DriverClass = this.spec.DriverClass || null;
|
|
856
|
-
agentEntity.ModelSelectionMode = this.spec.ModelSelectionMode || 'Agent Type';
|
|
857
|
-
|
|
858
|
-
// Handle TypeID - supports lookup references or direct GUID
|
|
859
|
-
if ((this.spec as any).TypeID) {
|
|
860
|
-
agentEntity.TypeID = (this.spec as any).TypeID;
|
|
861
|
-
}
|
|
780
|
+
// Step 2: Save actions
|
|
781
|
+
await this.saveActions(agentId);
|
|
862
782
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
agentEntity.Status = (this.spec as any).Status;
|
|
866
|
-
} else {
|
|
867
|
-
agentEntity.Status = 'Active';
|
|
868
|
-
}
|
|
783
|
+
// Step 3: Save sub-agents (both child and related)
|
|
784
|
+
await this.saveSubAgents(agentId);
|
|
869
785
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
this.spec.PayloadDownstreamPaths || ['*']
|
|
873
|
-
);
|
|
874
|
-
agentEntity.PayloadUpstreamPaths = JSON.stringify(
|
|
875
|
-
this.spec.PayloadUpstreamPaths || ['*']
|
|
876
|
-
);
|
|
877
|
-
agentEntity.PayloadSelfReadPaths = this.spec.PayloadSelfReadPaths
|
|
878
|
-
? JSON.stringify(this.spec.PayloadSelfReadPaths)
|
|
879
|
-
: null;
|
|
880
|
-
agentEntity.PayloadSelfWritePaths = this.spec.PayloadSelfWritePaths
|
|
881
|
-
? JSON.stringify(this.spec.PayloadSelfWritePaths)
|
|
882
|
-
: null;
|
|
883
|
-
agentEntity.PayloadScope = this.spec.PayloadScope || null;
|
|
884
|
-
|
|
885
|
-
// Validation fields
|
|
886
|
-
agentEntity.FinalPayloadValidation = this.spec.FinalPayloadValidation || null;
|
|
887
|
-
agentEntity.FinalPayloadValidationMode = this.spec.FinalPayloadValidationMode || 'Retry';
|
|
888
|
-
agentEntity.FinalPayloadValidationMaxRetries = this.spec.FinalPayloadValidationMaxRetries || 3;
|
|
889
|
-
|
|
890
|
-
agentEntity.StartingPayloadValidation = this.spec.StartingPayloadValidation || null;
|
|
891
|
-
agentEntity.StartingPayloadValidationMode = this.spec.StartingPayloadValidationMode || 'Fail';
|
|
892
|
-
|
|
893
|
-
// Resource limits
|
|
894
|
-
agentEntity.MaxCostPerRun = this.spec.MaxCostPerRun || null;
|
|
895
|
-
agentEntity.MaxTokensPerRun = this.spec.MaxTokensPerRun || null;
|
|
896
|
-
agentEntity.MaxIterationsPerRun = this.spec.MaxIterationsPerRun || null;
|
|
897
|
-
agentEntity.MaxTimePerRun = this.spec.MaxTimePerRun || null;
|
|
898
|
-
|
|
899
|
-
// Execution frequency
|
|
900
|
-
agentEntity.MinExecutionsPerRun = this.spec.MinExecutionsPerRun || null;
|
|
901
|
-
agentEntity.MaxExecutionsPerRun = this.spec.MaxExecutionsPerRun || null;
|
|
902
|
-
|
|
903
|
-
// Other config
|
|
904
|
-
agentEntity.DefaultPromptEffortLevel = this.spec.DefaultPromptEffortLevel || null;
|
|
905
|
-
agentEntity.ChatHandlingOption = this.spec.ChatHandlingOption || null;
|
|
906
|
-
agentEntity.DefaultArtifactTypeID = this.spec.DefaultArtifactTypeID || null;
|
|
907
|
-
|
|
908
|
-
// Set OwnerUserID - always use contextUser if available (user creating/modifying the agent)
|
|
909
|
-
if (this._contextUser) {
|
|
910
|
-
agentEntity.OwnerUserID = this._contextUser.ID;
|
|
911
|
-
} else if (this.spec.OwnerUserID) {
|
|
912
|
-
// Fallback to spec value if no contextUser provided
|
|
913
|
-
agentEntity.OwnerUserID = this.spec.OwnerUserID;
|
|
914
|
-
}
|
|
786
|
+
// Step 4: Save prompts
|
|
787
|
+
await this.savePrompts(agentId);
|
|
915
788
|
|
|
916
|
-
|
|
789
|
+
// Step 5: Save steps (Flow agents only)
|
|
790
|
+
await this.saveSteps(agentId);
|
|
917
791
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
agentEntity.TechnicalDesign = this.spec.TechnicalDesign || null;
|
|
792
|
+
// Step 6: Save step paths (Flow agents only)
|
|
793
|
+
await this.saveStepPaths(agentId);
|
|
921
794
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const validation = agentEntity.Validate();
|
|
925
|
-
if (!validation.Success) {
|
|
926
|
-
const errors = validation.Errors.map(e => e.Message).join(', ');
|
|
927
|
-
throw new Error(`Agent validation failed: ${errors}`);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// Save
|
|
932
|
-
const saved = await agentEntity.Save();
|
|
933
|
-
if (!saved) {
|
|
934
|
-
throw new Error('Failed to save agent entity');
|
|
935
|
-
}
|
|
795
|
+
this._isDirty = false;
|
|
796
|
+
this._isLoaded = true;
|
|
936
797
|
|
|
937
|
-
|
|
938
|
-
|
|
798
|
+
return {
|
|
799
|
+
agentId,
|
|
800
|
+
success: true,
|
|
801
|
+
mutations: this.getMutations(),
|
|
802
|
+
};
|
|
803
|
+
} catch (error) {
|
|
804
|
+
// Return failure with whatever mutations were completed before error
|
|
805
|
+
return {
|
|
806
|
+
agentId: this.spec.ID || '',
|
|
807
|
+
success: false,
|
|
808
|
+
mutations: this.getMutations(),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Save the main AIAgent entity.
|
|
815
|
+
*
|
|
816
|
+
* @private
|
|
817
|
+
* @param validate - Whether to perform validation before saving
|
|
818
|
+
* @returns Promise resolving to the saved agent ID
|
|
819
|
+
* @throws {Error} If validation fails or save fails
|
|
820
|
+
*/
|
|
821
|
+
private async saveAgentEntity(validate: boolean): Promise<string> {
|
|
822
|
+
const md = new Metadata();
|
|
823
|
+
const agentEntity = await md.GetEntityObject<AIAgentEntity>('AI Agents', this._contextUser);
|
|
824
|
+
|
|
825
|
+
// Track if this is an update or create
|
|
826
|
+
const isUpdate = !!this.spec.ID;
|
|
827
|
+
|
|
828
|
+
// If ID exists, load existing record
|
|
829
|
+
if (this.spec.ID) {
|
|
830
|
+
const loaded = await agentEntity.Load(this.spec.ID);
|
|
831
|
+
if (!loaded) {
|
|
832
|
+
throw new Error(`Cannot update non-existent agent with ID ${this.spec.ID}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
939
835
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
836
|
+
// Map spec to entity fields
|
|
837
|
+
agentEntity.Name = this.spec.Name;
|
|
838
|
+
agentEntity.Description = this.spec.Description || null;
|
|
839
|
+
agentEntity.IconClass = this.spec.IconClass || null;
|
|
840
|
+
agentEntity.LogoURL = this.spec.LogoURL || null;
|
|
841
|
+
agentEntity.ParentID = this.spec.ParentID || null;
|
|
842
|
+
agentEntity.DriverClass = this.spec.DriverClass || null;
|
|
843
|
+
agentEntity.ModelSelectionMode = this.spec.ModelSelectionMode || 'Agent Type';
|
|
844
|
+
|
|
845
|
+
// Handle TypeID - supports lookup references or direct GUID
|
|
846
|
+
if ((this.spec as any).TypeID) {
|
|
847
|
+
agentEntity.TypeID = (this.spec as any).TypeID;
|
|
848
|
+
}
|
|
947
849
|
|
|
948
|
-
|
|
850
|
+
// Handle Status - defaults to Active if not specified
|
|
851
|
+
if ((this.spec as any).Status) {
|
|
852
|
+
agentEntity.Status = (this.spec as any).Status;
|
|
853
|
+
} else {
|
|
854
|
+
agentEntity.Status = 'Active';
|
|
949
855
|
}
|
|
950
856
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
857
|
+
// Serialize JSON fields
|
|
858
|
+
agentEntity.PayloadDownstreamPaths = JSON.stringify(this.spec.PayloadDownstreamPaths || ['*']);
|
|
859
|
+
agentEntity.PayloadUpstreamPaths = JSON.stringify(this.spec.PayloadUpstreamPaths || ['*']);
|
|
860
|
+
agentEntity.PayloadSelfReadPaths = this.spec.PayloadSelfReadPaths ? JSON.stringify(this.spec.PayloadSelfReadPaths) : null;
|
|
861
|
+
agentEntity.PayloadSelfWritePaths = this.spec.PayloadSelfWritePaths ? JSON.stringify(this.spec.PayloadSelfWritePaths) : null;
|
|
862
|
+
agentEntity.PayloadScope = this.spec.PayloadScope || null;
|
|
863
|
+
|
|
864
|
+
// Validation fields
|
|
865
|
+
agentEntity.FinalPayloadValidation = this.spec.FinalPayloadValidation || null;
|
|
866
|
+
agentEntity.FinalPayloadValidationMode = this.spec.FinalPayloadValidationMode || 'Retry';
|
|
867
|
+
agentEntity.FinalPayloadValidationMaxRetries = this.spec.FinalPayloadValidationMaxRetries || 3;
|
|
868
|
+
|
|
869
|
+
agentEntity.StartingPayloadValidation = this.spec.StartingPayloadValidation || null;
|
|
870
|
+
agentEntity.StartingPayloadValidationMode = this.spec.StartingPayloadValidationMode || 'Fail';
|
|
871
|
+
|
|
872
|
+
// Resource limits
|
|
873
|
+
agentEntity.MaxCostPerRun = this.spec.MaxCostPerRun || null;
|
|
874
|
+
agentEntity.MaxTokensPerRun = this.spec.MaxTokensPerRun || null;
|
|
875
|
+
agentEntity.MaxIterationsPerRun = this.spec.MaxIterationsPerRun || null;
|
|
876
|
+
agentEntity.MaxTimePerRun = this.spec.MaxTimePerRun || null;
|
|
877
|
+
|
|
878
|
+
// Execution frequency
|
|
879
|
+
agentEntity.MinExecutionsPerRun = this.spec.MinExecutionsPerRun || null;
|
|
880
|
+
agentEntity.MaxExecutionsPerRun = this.spec.MaxExecutionsPerRun || null;
|
|
881
|
+
|
|
882
|
+
// Other config
|
|
883
|
+
agentEntity.DefaultPromptEffortLevel = this.spec.DefaultPromptEffortLevel || null;
|
|
884
|
+
agentEntity.ChatHandlingOption = this.spec.ChatHandlingOption || null;
|
|
885
|
+
agentEntity.DefaultArtifactTypeID = this.spec.DefaultArtifactTypeID || null;
|
|
886
|
+
|
|
887
|
+
// Set OwnerUserID - always use contextUser if available (user creating/modifying the agent)
|
|
888
|
+
if (this._contextUser) {
|
|
889
|
+
agentEntity.OwnerUserID = this._contextUser.ID;
|
|
890
|
+
} else if (this.spec.OwnerUserID) {
|
|
891
|
+
// Fallback to spec value if no contextUser provided
|
|
892
|
+
agentEntity.OwnerUserID = this.spec.OwnerUserID;
|
|
893
|
+
}
|
|
966
894
|
|
|
967
|
-
|
|
895
|
+
agentEntity.InvocationMode = this.spec.InvocationMode || 'Any';
|
|
968
896
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
this._contextUser
|
|
973
|
-
);
|
|
897
|
+
// Requirements and design documentation
|
|
898
|
+
agentEntity.FunctionalRequirements = this.spec.FunctionalRequirements || null;
|
|
899
|
+
agentEntity.TechnicalDesign = this.spec.TechnicalDesign || null;
|
|
974
900
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
// Map fields
|
|
984
|
-
actionEntity.AgentID = agentId;
|
|
985
|
-
actionEntity.ActionID = actionSpec.ActionID;
|
|
986
|
-
actionEntity.Status = actionSpec.Status;
|
|
987
|
-
actionEntity.MaxExecutionsPerRun = actionSpec.MaxExecutionsPerRun || null;
|
|
988
|
-
actionEntity.ResultExpirationTurns = actionSpec.ResultExpirationTurns || null;
|
|
989
|
-
actionEntity.ResultExpirationMode = actionSpec.ResultExpirationMode || 'None';
|
|
990
|
-
actionEntity.CompactMode = actionSpec.CompactMode || null;
|
|
991
|
-
actionEntity.CompactLength = actionSpec.CompactLength || null;
|
|
992
|
-
actionEntity.CompactPromptID = actionSpec.CompactPromptID || null;
|
|
993
|
-
|
|
994
|
-
const saved = await actionEntity.Save();
|
|
995
|
-
if (!saved) {
|
|
996
|
-
throw new Error(`Failed to save action ${actionSpec.ActionID}`);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Update spec with saved ID
|
|
1000
|
-
actionSpec.AgentActionID = actionEntity.ID;
|
|
1001
|
-
|
|
1002
|
-
// Track the mutation
|
|
1003
|
-
this.trackMutation(
|
|
1004
|
-
'AI Agent Actions',
|
|
1005
|
-
isUpdate ? 'Update' : 'Create',
|
|
1006
|
-
actionEntity.ID,
|
|
1007
|
-
`${isUpdate ? 'Updated' : 'Created'} action junction for action ${actionSpec.ActionID}`
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
901
|
+
// Validate if requested
|
|
902
|
+
if (validate) {
|
|
903
|
+
const validation = agentEntity.Validate();
|
|
904
|
+
if (!validation.Success) {
|
|
905
|
+
const errors = validation.Errors.map((e) => e.Message).join(', ');
|
|
906
|
+
throw new Error(`Agent validation failed: ${errors}`);
|
|
907
|
+
}
|
|
1010
908
|
}
|
|
1011
909
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
* For child agents, updates the ParentID on the sub-agent entity. For related agents,
|
|
1017
|
-
* creates or updates the relationship record.
|
|
1018
|
-
*
|
|
1019
|
-
* @private
|
|
1020
|
-
* @param agentId - The parent agent ID
|
|
1021
|
-
* @throws {Error} If any sub-agent save fails
|
|
1022
|
-
*/
|
|
1023
|
-
private async saveSubAgents(agentId: string): Promise<void> {
|
|
1024
|
-
if (!this.spec.SubAgents || this.spec.SubAgents.length === 0) {
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
for (const SubAgentSpec of this.spec.SubAgents) {
|
|
1029
|
-
if (SubAgentSpec.Type === 'child') {
|
|
1030
|
-
await this.saveChildSubAgent(agentId, SubAgentSpec);
|
|
1031
|
-
} else {
|
|
1032
|
-
await this.saveRelatedSubAgent(agentId, SubAgentSpec);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
910
|
+
// Save
|
|
911
|
+
const saved = await agentEntity.Save();
|
|
912
|
+
if (!saved) {
|
|
913
|
+
throw new Error('Failed to save agent entity');
|
|
1035
914
|
}
|
|
1036
915
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
916
|
+
// Update spec with saved ID
|
|
917
|
+
this.spec.ID = agentEntity.ID;
|
|
918
|
+
|
|
919
|
+
// Track the mutation
|
|
920
|
+
this.trackMutation(
|
|
921
|
+
'AI Agents',
|
|
922
|
+
isUpdate ? 'Update' : 'Create',
|
|
923
|
+
agentEntity.ID,
|
|
924
|
+
`${isUpdate ? 'Updated' : 'Created'} agent: ${this.spec.Name}`
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
return agentEntity.ID;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Save all actions for this agent.
|
|
932
|
+
*
|
|
933
|
+
* Creates or updates agent action records. Existing actions are updated, new actions
|
|
934
|
+
* are created. This method does not delete actions that are no longer in the spec -
|
|
935
|
+
* use a separate delete method for that.
|
|
936
|
+
*
|
|
937
|
+
* @private
|
|
938
|
+
* @param agentId - The parent agent ID
|
|
939
|
+
* @throws {Error} If any action save fails
|
|
940
|
+
*/
|
|
941
|
+
private async saveActions(agentId: string): Promise<void> {
|
|
942
|
+
if (!this.spec.Actions || this.spec.Actions.length === 0) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
1064
945
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
946
|
+
const md = new Metadata();
|
|
947
|
+
|
|
948
|
+
for (const actionSpec of this.spec.Actions) {
|
|
949
|
+
const actionEntity = await md.GetEntityObject<AIAgentActionEntity>('AI Agent Actions', this._contextUser);
|
|
950
|
+
|
|
951
|
+
// Track if this is an update or create
|
|
952
|
+
const isUpdate = !!actionSpec.AgentActionID;
|
|
953
|
+
|
|
954
|
+
// Load existing if ID provided
|
|
955
|
+
if (actionSpec.AgentActionID) {
|
|
956
|
+
await actionEntity.Load(actionSpec.AgentActionID);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Map fields
|
|
960
|
+
actionEntity.AgentID = agentId;
|
|
961
|
+
actionEntity.ActionID = actionSpec.ActionID;
|
|
962
|
+
actionEntity.Status = actionSpec.Status;
|
|
963
|
+
actionEntity.MaxExecutionsPerRun = actionSpec.MaxExecutionsPerRun || null;
|
|
964
|
+
actionEntity.ResultExpirationTurns = actionSpec.ResultExpirationTurns || null;
|
|
965
|
+
actionEntity.ResultExpirationMode = actionSpec.ResultExpirationMode || 'None';
|
|
966
|
+
actionEntity.CompactMode = actionSpec.CompactMode || null;
|
|
967
|
+
actionEntity.CompactLength = actionSpec.CompactLength || null;
|
|
968
|
+
actionEntity.CompactPromptID = actionSpec.CompactPromptID || null;
|
|
969
|
+
|
|
970
|
+
const saved = await actionEntity.Save();
|
|
971
|
+
if (!saved) {
|
|
972
|
+
throw new Error(`Failed to save action ${actionSpec.ActionID}`);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Update spec with saved ID
|
|
976
|
+
actionSpec.AgentActionID = actionEntity.ID;
|
|
977
|
+
|
|
978
|
+
// Track the mutation
|
|
979
|
+
this.trackMutation(
|
|
980
|
+
'AI Agent Actions',
|
|
981
|
+
isUpdate ? 'Update' : 'Create',
|
|
982
|
+
actionEntity.ID,
|
|
983
|
+
`${isUpdate ? 'Updated' : 'Created'} action junction for action ${actionSpec.ActionID}`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Save all sub-agents (both child and related types).
|
|
990
|
+
*
|
|
991
|
+
* Handles both ParentID-based child agents and relationship-based related agents.
|
|
992
|
+
* For child agents, updates the ParentID on the sub-agent entity. For related agents,
|
|
993
|
+
* creates or updates the relationship record.
|
|
994
|
+
*
|
|
995
|
+
* @private
|
|
996
|
+
* @param agentId - The parent agent ID
|
|
997
|
+
* @throws {Error} If any sub-agent save fails
|
|
998
|
+
*/
|
|
999
|
+
private async saveSubAgents(agentId: string): Promise<void> {
|
|
1000
|
+
if (!this.spec.SubAgents || this.spec.SubAgents.length === 0) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1069
1003
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1004
|
+
for (const SubAgentSpec of this.spec.SubAgents) {
|
|
1005
|
+
if (SubAgentSpec.Type === 'child') {
|
|
1006
|
+
await this.saveChildSubAgent(agentId, SubAgentSpec);
|
|
1007
|
+
} else {
|
|
1008
|
+
await this.saveRelatedSubAgent(agentId, SubAgentSpec);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Save a child sub-agent (ParentID-based relationship).
|
|
1015
|
+
*
|
|
1016
|
+
* For child agents, the relationship is established by setting the ParentID field
|
|
1017
|
+
* on the child agent entity. If the SubAgent.ID is empty, this creates a new
|
|
1018
|
+
* child agent recursively using AgentSpecSync.
|
|
1019
|
+
*
|
|
1020
|
+
* This enables creating complete agent hierarchies in one call - sub-agents are
|
|
1021
|
+
* created first (depth-first), then parent references them via ParentID.
|
|
1022
|
+
*
|
|
1023
|
+
* @private
|
|
1024
|
+
* @param parentId - The parent agent ID
|
|
1025
|
+
* @param SubAgentSpec - The sub-agent specification
|
|
1026
|
+
* @throws {Error} If child agent save fails
|
|
1027
|
+
*/
|
|
1028
|
+
private async saveChildSubAgent(parentId: string, SubAgentSpec: SubAgentSpec): Promise<void> {
|
|
1029
|
+
console.log(`🔗 saveChildSubAgent: Processing child "${SubAgentSpec.SubAgent?.Name}", ID="${SubAgentSpec.SubAgent?.ID}"`);
|
|
1030
|
+
|
|
1031
|
+
// If SubAgent.ID is empty or missing, create the sub-agent recursively
|
|
1032
|
+
if (!SubAgentSpec.SubAgent?.ID || SubAgentSpec.SubAgent.ID === '') {
|
|
1033
|
+
console.log(`🔨 saveChildSubAgent: Creating new child sub-agent "${SubAgentSpec.SubAgent.Name}"...`);
|
|
1034
|
+
|
|
1035
|
+
// Set ParentID in the sub-agent spec before creating it
|
|
1036
|
+
const childSpec: AgentSpec = {
|
|
1037
|
+
...SubAgentSpec.SubAgent,
|
|
1038
|
+
ParentID: parentId,
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// Recursively create the sub-agent using AgentSpecSync
|
|
1042
|
+
const childSync = new AgentSpecSync(childSpec, this._contextUser);
|
|
1043
|
+
childSync.markDirty();
|
|
1044
|
+
const childResult = await childSync.SaveToDatabase();
|
|
1045
|
+
|
|
1046
|
+
// Update the SubAgentSpec with the created ID so parent can reference it
|
|
1047
|
+
SubAgentSpec.SubAgent.ID = childResult.agentId;
|
|
1048
|
+
|
|
1049
|
+
console.log(`✅ saveChildSubAgent: Created child sub-agent with ID: ${childResult.agentId}`);
|
|
1050
|
+
} else {
|
|
1051
|
+
// SubAgent already exists - update it recursively to capture all field changes
|
|
1052
|
+
console.log(`🔗 saveChildSubAgent: Updating existing child sub-agent "${SubAgentSpec.SubAgent.ID}"...`);
|
|
1053
|
+
|
|
1054
|
+
// Set ParentID in the sub-agent spec
|
|
1055
|
+
const childSpec: AgentSpec = {
|
|
1056
|
+
...SubAgentSpec.SubAgent,
|
|
1057
|
+
ParentID: parentId,
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// Recursively update the sub-agent using AgentSpecSync
|
|
1061
|
+
// This ensures all fields including FunctionalRequirements and TechnicalDesign are updated
|
|
1062
|
+
const childSync = new AgentSpecSync(childSpec, this._contextUser);
|
|
1063
|
+
childSync.markDirty();
|
|
1064
|
+
childSync.markLoaded(); // Mark as loaded so delete logic runs for orphaned records
|
|
1065
|
+
await childSync.SaveToDatabase();
|
|
1066
|
+
|
|
1067
|
+
console.log(`✅ saveChildSubAgent: Updated existing child sub-agent`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Save a related sub-agent (relationship-based).
|
|
1073
|
+
*
|
|
1074
|
+
* Creates or updates an AIAgentRelationship record that links the parent and
|
|
1075
|
+
* sub-agent. This includes the input/output mapping and context path configurations.
|
|
1076
|
+
*
|
|
1077
|
+
* @private
|
|
1078
|
+
* @param agentId - The parent agent ID
|
|
1079
|
+
* @param SubAgentSpec - The sub-agent specification
|
|
1080
|
+
* @throws {Error} If relationship save fails
|
|
1081
|
+
*/
|
|
1082
|
+
private async saveRelatedSubAgent(agentId: string, SubAgentSpec: SubAgentSpec): Promise<void> {
|
|
1083
|
+
const md = new Metadata();
|
|
1084
|
+
const relationshipEntity = await md.GetEntityObject<AIAgentRelationshipEntity>('MJ: AI Agent Relationships', this._contextUser);
|
|
1085
|
+
|
|
1086
|
+
// Load existing if ID provided
|
|
1087
|
+
const isUpdate = !!SubAgentSpec.AgentRelationshipID;
|
|
1088
|
+
if (isUpdate && SubAgentSpec.AgentRelationshipID) {
|
|
1089
|
+
await relationshipEntity.Load(SubAgentSpec.AgentRelationshipID);
|
|
1090
|
+
}
|
|
1072
1091
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1092
|
+
// Map fields
|
|
1093
|
+
relationshipEntity.AgentID = agentId;
|
|
1094
|
+
relationshipEntity.SubAgentID = SubAgentSpec.SubAgent.ID;
|
|
1095
|
+
relationshipEntity.Status = 'Active';
|
|
1077
1096
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1097
|
+
// Serialize mapping fields
|
|
1098
|
+
if (SubAgentSpec.SubAgentInputMapping) {
|
|
1099
|
+
relationshipEntity.SubAgentInputMapping = JSON.stringify(SubAgentSpec.SubAgentInputMapping);
|
|
1100
|
+
} else {
|
|
1101
|
+
relationshipEntity.SubAgentInputMapping = null;
|
|
1102
|
+
}
|
|
1083
1103
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
await childSync.SaveToDatabase();
|
|
1104
|
+
if (SubAgentSpec.SubAgentOutputMapping) {
|
|
1105
|
+
relationshipEntity.SubAgentOutputMapping = JSON.stringify(SubAgentSpec.SubAgentOutputMapping);
|
|
1106
|
+
} else {
|
|
1107
|
+
relationshipEntity.SubAgentOutputMapping = null;
|
|
1108
|
+
}
|
|
1090
1109
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1110
|
+
if (SubAgentSpec.SubAgentContextPaths) {
|
|
1111
|
+
relationshipEntity.SubAgentContextPaths = JSON.stringify(SubAgentSpec.SubAgentContextPaths);
|
|
1112
|
+
} else {
|
|
1113
|
+
relationshipEntity.SubAgentContextPaths = null;
|
|
1093
1114
|
}
|
|
1094
1115
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
* sub-agent. This includes the input/output mapping and context path configurations.
|
|
1100
|
-
*
|
|
1101
|
-
* @private
|
|
1102
|
-
* @param agentId - The parent agent ID
|
|
1103
|
-
* @param SubAgentSpec - The sub-agent specification
|
|
1104
|
-
* @throws {Error} If relationship save fails
|
|
1105
|
-
*/
|
|
1106
|
-
private async saveRelatedSubAgent(agentId: string, SubAgentSpec: SubAgentSpec): Promise<void> {
|
|
1107
|
-
const md = new Metadata();
|
|
1108
|
-
const relationshipEntity = await md.GetEntityObject<AIAgentRelationshipEntity>(
|
|
1109
|
-
'MJ: AI Agent Relationships',
|
|
1110
|
-
this._contextUser
|
|
1111
|
-
);
|
|
1116
|
+
const saved = await relationshipEntity.Save();
|
|
1117
|
+
if (!saved) {
|
|
1118
|
+
throw new Error(`Failed to save relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`);
|
|
1119
|
+
}
|
|
1112
1120
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1121
|
+
// Track mutation
|
|
1122
|
+
this.trackMutation(
|
|
1123
|
+
'AI Agent Relationships',
|
|
1124
|
+
isUpdate ? 'Update' : 'Create',
|
|
1125
|
+
relationshipEntity.ID,
|
|
1126
|
+
`${isUpdate ? 'Updated' : 'Created'} agent relationship for sub-agent ${SubAgentSpec.SubAgent.ID}`
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
// Update spec with saved ID
|
|
1130
|
+
SubAgentSpec.AgentRelationshipID = relationshipEntity.ID;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Save all prompts for this agent.
|
|
1135
|
+
*
|
|
1136
|
+
* Creates AIPrompt records (with template) and AIAgentPrompt junction records.
|
|
1137
|
+
* Supports simplified prompt format from Architect Agent with just PromptText,
|
|
1138
|
+
* PromptRole, and PromptPosition.
|
|
1139
|
+
*
|
|
1140
|
+
* @private
|
|
1141
|
+
* @param agentId - The parent agent ID
|
|
1142
|
+
* @throws {Error} If any prompt save fails
|
|
1143
|
+
*/
|
|
1144
|
+
private async savePrompts(agentId: string): Promise<void> {
|
|
1145
|
+
console.log(`💬 savePrompts: Called with agentId=${agentId}, Prompts=${this.spec.Prompts ? this.spec.Prompts.length : 'undefined'}`);
|
|
1146
|
+
|
|
1147
|
+
if (!this.spec.Prompts || this.spec.Prompts.length === 0) {
|
|
1148
|
+
console.log('💬 savePrompts: No prompts to save, returning early');
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1118
1151
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
relationshipEntity.Status = 'Active';
|
|
1152
|
+
console.log(`💬 savePrompts: Processing ${this.spec.Prompts.length} prompt(s)...`);
|
|
1153
|
+
const md = new Metadata();
|
|
1154
|
+
const rv = new RunView();
|
|
1123
1155
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
} else {
|
|
1128
|
-
relationshipEntity.SubAgentInputMapping = null;
|
|
1129
|
-
}
|
|
1156
|
+
// Step 1: Create or update AIPrompt records
|
|
1157
|
+
for (let i = 0; i < this.spec.Prompts.length; i++) {
|
|
1158
|
+
const promptSpec = this.spec.Prompts[i];
|
|
1130
1159
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
} else {
|
|
1134
|
-
relationshipEntity.SubAgentOutputMapping = null;
|
|
1135
|
-
}
|
|
1160
|
+
// Check if this is an update (has PromptID) or create (no PromptID)
|
|
1161
|
+
let promptEntity = await md.GetEntityObject<any>('AI Prompts', this._contextUser);
|
|
1136
1162
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1163
|
+
let isUpdate = false;
|
|
1164
|
+
if ((promptSpec as any).PromptID && (promptSpec as any).PromptID !== '') {
|
|
1165
|
+
// Load existing prompt for update
|
|
1166
|
+
const loaded = await promptEntity.Load((promptSpec as any).PromptID);
|
|
1167
|
+
if (loaded) {
|
|
1168
|
+
isUpdate = true;
|
|
1169
|
+
console.log(`💬 savePrompts: Updating existing prompt ID: ${(promptSpec as any).PromptID}`);
|
|
1139
1170
|
} else {
|
|
1140
|
-
|
|
1171
|
+
console.warn(`💬 savePrompts: PromptID specified but not found, creating new prompt`);
|
|
1141
1172
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Set all required fields (both create and update)
|
|
1176
|
+
promptEntity.Name = `${this.spec.Name} - Prompt ${i + 1}`;
|
|
1177
|
+
promptEntity.Description = `Agent prompt ${i + 1} for ${this.spec.Name}`;
|
|
1178
|
+
promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
|
|
1179
|
+
promptEntity.Status = 'Active';
|
|
1180
|
+
promptEntity.ResponseFormat = 'JSON';
|
|
1181
|
+
|
|
1182
|
+
// Handle prompt text - supports both string and object formats
|
|
1183
|
+
if (typeof (promptSpec as any).PromptText === 'string') {
|
|
1184
|
+
promptEntity.TemplateText = (promptSpec as any).PromptText;
|
|
1185
|
+
} else if (typeof (promptSpec as any).PromptText === 'object') {
|
|
1186
|
+
// Architect may send PromptText as {text: "...", json: {...}}
|
|
1187
|
+
const promptTextObj = (promptSpec as any).PromptText as any;
|
|
1188
|
+
let combinedText = promptTextObj.text || '';
|
|
1189
|
+
if (promptTextObj.json) {
|
|
1190
|
+
combinedText += '\n\n```json\n' + JSON.stringify(promptTextObj.json, null, 2) + '\n```';
|
|
1146
1191
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1192
|
+
promptEntity.TemplateText = combinedText;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Set prompt role and position if provided
|
|
1196
|
+
if ((promptSpec as any).PromptRole) {
|
|
1197
|
+
promptEntity.PromptRole = (promptSpec as any).PromptRole;
|
|
1198
|
+
}
|
|
1199
|
+
if ((promptSpec as any).PromptPosition) {
|
|
1200
|
+
promptEntity.PromptPosition = (promptSpec as any).PromptPosition;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const saved = await promptEntity.Save();
|
|
1204
|
+
if (!saved) {
|
|
1205
|
+
throw new Error(`Failed to save prompt ${i + 1} for agent ${this.spec.Name}`);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Track mutation
|
|
1209
|
+
this.trackMutation(
|
|
1210
|
+
'AI Prompts',
|
|
1211
|
+
isUpdate ? 'Update' : 'Create',
|
|
1212
|
+
promptEntity.ID,
|
|
1213
|
+
`${isUpdate ? 'Updated' : 'Created'} prompt: ${promptEntity.Name}`
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
// Update the spec with the saved/updated ID
|
|
1217
|
+
(promptSpec as any).PromptID = promptEntity.ID;
|
|
1218
|
+
|
|
1219
|
+
console.log(`✅ savePrompts: ${isUpdate ? 'Updated' : 'Created'} AIPrompt with ID: ${promptEntity.ID}`);
|
|
1220
|
+
|
|
1221
|
+
// Step 2: Create or update AIAgentPrompt junction
|
|
1222
|
+
// Query for existing junction by AgentID + PromptID
|
|
1223
|
+
const existingJunctionResult = await rv.RunView<any>(
|
|
1224
|
+
{
|
|
1225
|
+
EntityName: 'MJ: AI Agent Prompts',
|
|
1226
|
+
ExtraFilter: `AgentID='${agentId}' AND PromptID='${promptEntity.ID}'`,
|
|
1227
|
+
ResultType: 'entity_object',
|
|
1228
|
+
},
|
|
1229
|
+
this._contextUser
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
let agentPromptEntity: any;
|
|
1233
|
+
let isJunctionUpdate = false;
|
|
1234
|
+
|
|
1235
|
+
if (existingJunctionResult.Success && existingJunctionResult.Results && existingJunctionResult.Results.length > 0) {
|
|
1236
|
+
// Junction exists - load it for update
|
|
1237
|
+
agentPromptEntity = existingJunctionResult.Results[0];
|
|
1238
|
+
isJunctionUpdate = true;
|
|
1239
|
+
console.log(`💬 savePrompts: Updating existing junction for prompt ${i + 1}`);
|
|
1240
|
+
} else {
|
|
1241
|
+
// Junction doesn't exist - create new one
|
|
1242
|
+
agentPromptEntity = await md.GetEntityObject<any>('MJ: AI Agent Prompts', this._contextUser);
|
|
1243
|
+
agentPromptEntity.AgentID = agentId;
|
|
1244
|
+
agentPromptEntity.PromptID = promptEntity.ID;
|
|
1245
|
+
console.log(`💬 savePrompts: Creating new junction for prompt ${i + 1}`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Set/update common fields (ExecutionOrder might change)
|
|
1249
|
+
agentPromptEntity.ExecutionOrder = i;
|
|
1250
|
+
agentPromptEntity.Status = 'Active';
|
|
1251
|
+
|
|
1252
|
+
const junctionSaved = await agentPromptEntity.Save();
|
|
1253
|
+
if (!junctionSaved) {
|
|
1254
|
+
throw new Error(`Failed to save agent-prompt junction for prompt ${i + 1}`);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Track mutation
|
|
1258
|
+
this.trackMutation(
|
|
1259
|
+
'AI Agent Prompts',
|
|
1260
|
+
isJunctionUpdate ? 'Update' : 'Create',
|
|
1261
|
+
agentPromptEntity.ID,
|
|
1262
|
+
`${isJunctionUpdate ? 'Updated' : 'Created'} agent-prompt junction for prompt ${promptEntity.ID}`
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
console.log(`✅ savePrompts: ${isJunctionUpdate ? 'Updated' : 'Created'} AIAgentPrompt junction with ID: ${agentPromptEntity.ID}`);
|
|
1158
1266
|
}
|
|
1159
1267
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
console.log(`💬 savePrompts: Processing ${this.spec.Prompts.length} prompt(s)...`);
|
|
1180
|
-
const md = new Metadata();
|
|
1181
|
-
const rv = new RunView();
|
|
1182
|
-
|
|
1183
|
-
// Step 1: Create or update AIPrompt records
|
|
1184
|
-
for (let i = 0; i < this.spec.Prompts.length; i++) {
|
|
1185
|
-
const promptSpec = this.spec.Prompts[i];
|
|
1186
|
-
|
|
1187
|
-
// Check if this is an update (has PromptID) or create (no PromptID)
|
|
1188
|
-
let promptEntity = await md.GetEntityObject<any>(
|
|
1189
|
-
'AI Prompts',
|
|
1190
|
-
this._contextUser
|
|
1191
|
-
);
|
|
1192
|
-
|
|
1193
|
-
let isUpdate = false;
|
|
1194
|
-
if ((promptSpec as any).PromptID && (promptSpec as any).PromptID !== '') {
|
|
1195
|
-
// Load existing prompt for update
|
|
1196
|
-
const loaded = await promptEntity.Load((promptSpec as any).PromptID);
|
|
1197
|
-
if (loaded) {
|
|
1198
|
-
isUpdate = true;
|
|
1199
|
-
console.log(`💬 savePrompts: Updating existing prompt ID: ${(promptSpec as any).PromptID}`);
|
|
1200
|
-
} else {
|
|
1201
|
-
console.warn(`💬 savePrompts: PromptID specified but not found, creating new prompt`);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Set all required fields (both create and update)
|
|
1206
|
-
promptEntity.Name = `${this.spec.Name} - Prompt ${i + 1}`;
|
|
1207
|
-
promptEntity.Description = `Agent prompt ${i + 1} for ${this.spec.Name}`;
|
|
1208
|
-
promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
|
|
1209
|
-
promptEntity.Status = 'Active';
|
|
1210
|
-
promptEntity.ResponseFormat = 'JSON';
|
|
1211
|
-
|
|
1212
|
-
// Handle prompt text - supports both string and object formats
|
|
1213
|
-
if (typeof (promptSpec as any).PromptText === 'string') {
|
|
1214
|
-
promptEntity.TemplateText = (promptSpec as any).PromptText;
|
|
1215
|
-
} else if (typeof (promptSpec as any).PromptText === 'object') {
|
|
1216
|
-
// Architect may send PromptText as {text: "...", json: {...}}
|
|
1217
|
-
const promptTextObj = (promptSpec as any).PromptText as any;
|
|
1218
|
-
let combinedText = promptTextObj.text || '';
|
|
1219
|
-
if (promptTextObj.json) {
|
|
1220
|
-
combinedText += '\n\n```json\n' + JSON.stringify(promptTextObj.json, null, 2) + '\n```';
|
|
1221
|
-
}
|
|
1222
|
-
promptEntity.TemplateText = combinedText;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// Set prompt role and position if provided
|
|
1226
|
-
if ((promptSpec as any).PromptRole) {
|
|
1227
|
-
promptEntity.PromptRole = (promptSpec as any).PromptRole;
|
|
1228
|
-
}
|
|
1229
|
-
if ((promptSpec as any).PromptPosition) {
|
|
1230
|
-
promptEntity.PromptPosition = (promptSpec as any).PromptPosition;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const saved = await promptEntity.Save();
|
|
1234
|
-
if (!saved) {
|
|
1235
|
-
throw new Error(`Failed to save prompt ${i + 1} for agent ${this.spec.Name}`);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// Track mutation
|
|
1239
|
-
this.trackMutation(
|
|
1240
|
-
'AI Prompts',
|
|
1241
|
-
isUpdate ? 'Update' : 'Create',
|
|
1242
|
-
promptEntity.ID,
|
|
1243
|
-
`${isUpdate ? 'Updated' : 'Created'} prompt: ${promptEntity.Name}`
|
|
1244
|
-
);
|
|
1245
|
-
|
|
1246
|
-
// Update the spec with the saved/updated ID
|
|
1247
|
-
(promptSpec as any).PromptID = promptEntity.ID;
|
|
1248
|
-
|
|
1249
|
-
console.log(`✅ savePrompts: ${isUpdate ? 'Updated' : 'Created'} AIPrompt with ID: ${promptEntity.ID}`);
|
|
1250
|
-
|
|
1251
|
-
// Step 2: Create or update AIAgentPrompt junction
|
|
1252
|
-
// Query for existing junction by AgentID + PromptID
|
|
1253
|
-
const existingJunctionResult = await rv.RunView<any>({
|
|
1254
|
-
EntityName: 'MJ: AI Agent Prompts',
|
|
1255
|
-
ExtraFilter: `AgentID='${agentId}' AND PromptID='${promptEntity.ID}'`,
|
|
1256
|
-
ResultType: 'entity_object'
|
|
1257
|
-
}, this._contextUser);
|
|
1258
|
-
|
|
1259
|
-
let agentPromptEntity: any;
|
|
1260
|
-
let isJunctionUpdate = false;
|
|
1261
|
-
|
|
1262
|
-
if (existingJunctionResult.Success && existingJunctionResult.Results && existingJunctionResult.Results.length > 0) {
|
|
1263
|
-
// Junction exists - load it for update
|
|
1264
|
-
agentPromptEntity = existingJunctionResult.Results[0];
|
|
1265
|
-
isJunctionUpdate = true;
|
|
1266
|
-
console.log(`💬 savePrompts: Updating existing junction for prompt ${i + 1}`);
|
|
1267
|
-
} else {
|
|
1268
|
-
// Junction doesn't exist - create new one
|
|
1269
|
-
agentPromptEntity = await md.GetEntityObject<any>(
|
|
1270
|
-
'MJ: AI Agent Prompts',
|
|
1271
|
-
this._contextUser
|
|
1272
|
-
);
|
|
1273
|
-
agentPromptEntity.AgentID = agentId;
|
|
1274
|
-
agentPromptEntity.PromptID = promptEntity.ID;
|
|
1275
|
-
console.log(`💬 savePrompts: Creating new junction for prompt ${i + 1}`);
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// Set/update common fields (ExecutionOrder might change)
|
|
1279
|
-
agentPromptEntity.ExecutionOrder = i;
|
|
1280
|
-
agentPromptEntity.Status = 'Active';
|
|
1281
|
-
|
|
1282
|
-
const junctionSaved = await agentPromptEntity.Save();
|
|
1283
|
-
if (!junctionSaved) {
|
|
1284
|
-
throw new Error(`Failed to save agent-prompt junction for prompt ${i + 1}`);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Track mutation
|
|
1288
|
-
this.trackMutation(
|
|
1289
|
-
'AI Agent Prompts',
|
|
1290
|
-
isJunctionUpdate ? 'Update' : 'Create',
|
|
1291
|
-
agentPromptEntity.ID,
|
|
1292
|
-
`${isJunctionUpdate ? 'Updated' : 'Created'} agent-prompt junction for prompt ${promptEntity.ID}`
|
|
1293
|
-
);
|
|
1294
|
-
|
|
1295
|
-
console.log(`✅ savePrompts: ${isJunctionUpdate ? 'Updated' : 'Created'} AIAgentPrompt junction with ID: ${agentPromptEntity.ID}`);
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
console.log(`✅ savePrompts: Successfully saved all ${this.spec.Prompts.length} prompt(s)`);
|
|
1268
|
+
console.log(`✅ savePrompts: Successfully saved all ${this.spec.Prompts.length} prompt(s)`);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Save all steps for this agent (Flow agents only).
|
|
1273
|
+
*
|
|
1274
|
+
* Creates AIAgentStep records for each step in the spec. Steps define the nodes
|
|
1275
|
+
* in a Flow agent's execution graph.
|
|
1276
|
+
*
|
|
1277
|
+
* @private
|
|
1278
|
+
* @param agentId - The parent agent ID
|
|
1279
|
+
* @throws {Error} If any step save fails
|
|
1280
|
+
*/
|
|
1281
|
+
private async saveSteps(agentId: string): Promise<void> {
|
|
1282
|
+
console.log(`🔷 saveSteps: Called with agentId=${agentId}, Steps=${this.spec.Steps ? this.spec.Steps.length : 'undefined'}`);
|
|
1283
|
+
|
|
1284
|
+
if (!this.spec.Steps || this.spec.Steps.length === 0) {
|
|
1285
|
+
console.log('🔷 saveSteps: No steps to save, returning early');
|
|
1286
|
+
return;
|
|
1299
1287
|
}
|
|
1300
1288
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
// Set prompt fields
|
|
1357
|
-
promptEntity.Name = stepSpec.PromptName || `${stepSpec.Name} Prompt`;
|
|
1358
|
-
promptEntity.Description = stepSpec.PromptDescription || `Prompt for step: ${stepSpec.Name}`;
|
|
1359
|
-
promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
|
|
1360
|
-
promptEntity.Status = 'Active';
|
|
1361
|
-
promptEntity.ResponseFormat = 'JSON';
|
|
1362
|
-
promptEntity.TemplateText = stepSpec.PromptText; // TemplateText is a dynamic property
|
|
1363
|
-
|
|
1364
|
-
const promptSaved = await promptEntity.Save();
|
|
1365
|
-
if (!promptSaved) {
|
|
1366
|
-
throw new Error(`Failed to save inline prompt for step "${stepSpec.Name}"`);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// Track mutation
|
|
1370
|
-
this.trackMutation(
|
|
1371
|
-
'AI Prompts',
|
|
1372
|
-
'Create',
|
|
1373
|
-
promptEntity.ID,
|
|
1374
|
-
`Created inline prompt: ${promptEntity.Name}`
|
|
1375
|
-
);
|
|
1376
|
-
|
|
1377
|
-
console.log(`✅ saveSteps: Created inline AIPrompt with ID: ${promptEntity.ID}`);
|
|
1378
|
-
|
|
1379
|
-
// Link the created prompt to this step
|
|
1380
|
-
stepEntity.PromptID = promptEntity.ID;
|
|
1381
|
-
stepSpec.PromptID = promptEntity.ID; // Update spec too
|
|
1382
|
-
} else {
|
|
1383
|
-
console.warn(`⚠️ saveSteps: Step "${stepSpec.Name}" is Prompt type with empty PromptID but no PromptText provided`);
|
|
1384
|
-
stepEntity.PromptID = null;
|
|
1385
|
-
}
|
|
1386
|
-
} else {
|
|
1387
|
-
stepEntity.PromptID = stepSpec.PromptID || null;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Map Action I/O mappings for Flow agent action steps
|
|
1391
|
-
// Handle ActionInputMapping - convert object to JSON string if needed
|
|
1392
|
-
if (stepSpec.ActionInputMapping) {
|
|
1393
|
-
stepEntity.ActionInputMapping = typeof stepSpec.ActionInputMapping === 'string'
|
|
1394
|
-
? stepSpec.ActionInputMapping
|
|
1395
|
-
: JSON.stringify(stepSpec.ActionInputMapping);
|
|
1396
|
-
} else {
|
|
1397
|
-
stepEntity.ActionInputMapping = null;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// Handle ActionOutputMapping - convert object to JSON string if needed
|
|
1401
|
-
if (stepSpec.ActionOutputMapping) {
|
|
1402
|
-
stepEntity.ActionOutputMapping = typeof stepSpec.ActionOutputMapping === 'string'
|
|
1403
|
-
? stepSpec.ActionOutputMapping
|
|
1404
|
-
: JSON.stringify(stepSpec.ActionOutputMapping);
|
|
1405
|
-
} else {
|
|
1406
|
-
stepEntity.ActionOutputMapping = null;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
const saved = await stepEntity.Save();
|
|
1410
|
-
if (!saved) {
|
|
1411
|
-
throw new Error(`Failed to save step "${stepSpec.Name}" for agent ${this.spec.Name}`);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Track mutation
|
|
1415
|
-
this.trackMutation(
|
|
1416
|
-
'AI Agent Steps',
|
|
1417
|
-
isUpdate ? 'Update' : 'Create',
|
|
1418
|
-
stepEntity.ID,
|
|
1419
|
-
`${isUpdate ? 'Updated' : 'Created'} step: ${stepSpec.Name}`
|
|
1420
|
-
);
|
|
1421
|
-
|
|
1422
|
-
// Update the spec with the saved ID so paths can reference it
|
|
1423
|
-
stepSpec.ID = stepEntity.ID;
|
|
1424
|
-
|
|
1425
|
-
console.log(`✅ saveSteps: Created AIAgentStep with ID: ${stepEntity.ID}`);
|
|
1289
|
+
console.log(`🔷 saveSteps: Processing ${this.spec.Steps.length} step(s)...`);
|
|
1290
|
+
const md = new Metadata();
|
|
1291
|
+
|
|
1292
|
+
for (const stepSpec of this.spec.Steps) {
|
|
1293
|
+
const stepEntity = await md.GetEntityObject<AIAgentStepEntity>('MJ: AI Agent Steps', this._contextUser);
|
|
1294
|
+
|
|
1295
|
+
// Load existing if ID provided
|
|
1296
|
+
const isUpdate = !!(stepSpec.ID && stepSpec.ID !== '');
|
|
1297
|
+
if (isUpdate) {
|
|
1298
|
+
await stepEntity.Load(stepSpec.ID);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Map fields
|
|
1302
|
+
stepEntity.AgentID = agentId;
|
|
1303
|
+
stepEntity.Name = stepSpec.Name;
|
|
1304
|
+
stepEntity.Description = stepSpec.Description || null;
|
|
1305
|
+
stepEntity.StepType = stepSpec.StepType;
|
|
1306
|
+
stepEntity.StartingStep = stepSpec.StartingStep;
|
|
1307
|
+
stepEntity.ActionID = stepSpec.ActionID || null;
|
|
1308
|
+
stepEntity.SubAgentID = stepSpec.SubAgentID || null;
|
|
1309
|
+
stepEntity.Status = 'Active'; // Default to Active
|
|
1310
|
+
|
|
1311
|
+
// Handle inline prompt creation for Prompt-type steps
|
|
1312
|
+
// If StepType is Prompt and PromptID is empty, create a new AIPrompt record
|
|
1313
|
+
if (stepSpec.StepType === 'Prompt' && (!stepSpec.PromptID || stepSpec.PromptID === '')) {
|
|
1314
|
+
if (stepSpec.PromptText) {
|
|
1315
|
+
console.log(`🔷 saveSteps: Creating inline prompt for step "${stepSpec.Name}"`);
|
|
1316
|
+
|
|
1317
|
+
// Create AIPrompt entity (using any type for TemplateText dynamic property)
|
|
1318
|
+
const promptEntity = await md.GetEntityObject<any>('AI Prompts', this._contextUser);
|
|
1319
|
+
|
|
1320
|
+
// Set prompt fields
|
|
1321
|
+
promptEntity.Name = stepSpec.PromptName || `${stepSpec.Name} Prompt`;
|
|
1322
|
+
promptEntity.Description = stepSpec.PromptDescription || `Prompt for step: ${stepSpec.Name}`;
|
|
1323
|
+
promptEntity.TypeID = 'a6da423e-f36b-1410-8dac-00021f8b792e'; // Chat type
|
|
1324
|
+
promptEntity.Status = 'Active';
|
|
1325
|
+
promptEntity.ResponseFormat = 'JSON';
|
|
1326
|
+
promptEntity.TemplateText = stepSpec.PromptText; // TemplateText is a dynamic property
|
|
1327
|
+
|
|
1328
|
+
const promptSaved = await promptEntity.Save();
|
|
1329
|
+
if (!promptSaved) {
|
|
1330
|
+
throw new Error(`Failed to save inline prompt for step "${stepSpec.Name}"`);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Track mutation
|
|
1334
|
+
this.trackMutation('AI Prompts', 'Create', promptEntity.ID, `Created inline prompt: ${promptEntity.Name}`);
|
|
1335
|
+
|
|
1336
|
+
console.log(`✅ saveSteps: Created inline AIPrompt with ID: ${promptEntity.ID}`);
|
|
1337
|
+
|
|
1338
|
+
// Link the created prompt to this step
|
|
1339
|
+
stepEntity.PromptID = promptEntity.ID;
|
|
1340
|
+
stepSpec.PromptID = promptEntity.ID; // Update spec too
|
|
1341
|
+
} else {
|
|
1342
|
+
console.warn(`⚠️ saveSteps: Step "${stepSpec.Name}" is Prompt type with empty PromptID but no PromptText provided`);
|
|
1343
|
+
stepEntity.PromptID = null;
|
|
1426
1344
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1345
|
+
} else {
|
|
1346
|
+
stepEntity.PromptID = stepSpec.PromptID || null;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Map Action I/O mappings for Flow agent action steps
|
|
1350
|
+
// Handle ActionInputMapping - convert object to JSON string if needed
|
|
1351
|
+
if (stepSpec.ActionInputMapping) {
|
|
1352
|
+
stepEntity.ActionInputMapping =
|
|
1353
|
+
typeof stepSpec.ActionInputMapping === 'string' ? stepSpec.ActionInputMapping : JSON.stringify(stepSpec.ActionInputMapping);
|
|
1354
|
+
} else {
|
|
1355
|
+
stepEntity.ActionInputMapping = null;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Handle ActionOutputMapping - convert object to JSON string if needed
|
|
1359
|
+
if (stepSpec.ActionOutputMapping) {
|
|
1360
|
+
stepEntity.ActionOutputMapping =
|
|
1361
|
+
typeof stepSpec.ActionOutputMapping === 'string' ? stepSpec.ActionOutputMapping : JSON.stringify(stepSpec.ActionOutputMapping);
|
|
1362
|
+
} else {
|
|
1363
|
+
stepEntity.ActionOutputMapping = null;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const saved = await stepEntity.Save();
|
|
1367
|
+
if (!saved) {
|
|
1368
|
+
throw new Error(`Failed to save step "${stepSpec.Name}" for agent ${this.spec.Name}`);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Track mutation
|
|
1372
|
+
this.trackMutation(
|
|
1373
|
+
'AI Agent Steps',
|
|
1374
|
+
isUpdate ? 'Update' : 'Create',
|
|
1375
|
+
stepEntity.ID,
|
|
1376
|
+
`${isUpdate ? 'Updated' : 'Created'} step: ${stepSpec.Name}`
|
|
1377
|
+
);
|
|
1378
|
+
|
|
1379
|
+
// Update the spec with the saved ID so paths can reference it
|
|
1380
|
+
stepSpec.ID = stepEntity.ID;
|
|
1381
|
+
|
|
1382
|
+
console.log(`✅ saveSteps: Created AIAgentStep with ID: ${stepEntity.ID}`);
|
|
1429
1383
|
}
|
|
1430
1384
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
// Create a map of step names to IDs for easy lookup
|
|
1453
|
-
const stepNameToId = new Map<string, string>();
|
|
1454
|
-
if (this.spec.Steps) {
|
|
1455
|
-
for (const step of this.spec.Steps) {
|
|
1456
|
-
if (step.ID) {
|
|
1457
|
-
stepNameToId.set(step.Name, step.ID);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
for (const pathSpec of this.spec.Paths) {
|
|
1463
|
-
const pathEntity = await md.GetEntityObject<AIAgentStepPathEntity>(
|
|
1464
|
-
'MJ: AI Agent Step Paths',
|
|
1465
|
-
this._contextUser
|
|
1466
|
-
);
|
|
1467
|
-
|
|
1468
|
-
// Load existing if ID provided
|
|
1469
|
-
const isUpdate = !!(pathSpec.ID && pathSpec.ID !== '');
|
|
1470
|
-
if (isUpdate) {
|
|
1471
|
-
await pathEntity.Load(pathSpec.ID);
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
// Resolve step IDs from step names
|
|
1475
|
-
// OriginStepID and DestinationStepID can be either step IDs or step names
|
|
1476
|
-
// If they're names, look them up in our map
|
|
1477
|
-
let originStepId = pathSpec.OriginStepID;
|
|
1478
|
-
let destinationStepId = pathSpec.DestinationStepID;
|
|
1479
|
-
|
|
1480
|
-
// Try to resolve as step names first
|
|
1481
|
-
if (stepNameToId.has(pathSpec.OriginStepID)) {
|
|
1482
|
-
originStepId = stepNameToId.get(pathSpec.OriginStepID)!;
|
|
1483
|
-
}
|
|
1484
|
-
if (stepNameToId.has(pathSpec.DestinationStepID)) {
|
|
1485
|
-
destinationStepId = stepNameToId.get(pathSpec.DestinationStepID)!;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
// Map fields
|
|
1489
|
-
pathEntity.OriginStepID = originStepId;
|
|
1490
|
-
pathEntity.DestinationStepID = destinationStepId;
|
|
1491
|
-
pathEntity.Condition = pathSpec.Condition || null;
|
|
1492
|
-
pathEntity.Description = pathSpec.Description || null;
|
|
1493
|
-
pathEntity.Priority = pathSpec.Priority;
|
|
1494
|
-
|
|
1495
|
-
const saved = await pathEntity.Save();
|
|
1496
|
-
if (!saved) {
|
|
1497
|
-
throw new Error(`Failed to save path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
// Track mutation
|
|
1501
|
-
this.trackMutation(
|
|
1502
|
-
'AI Agent Step Paths',
|
|
1503
|
-
isUpdate ? 'Update' : 'Create',
|
|
1504
|
-
pathEntity.ID,
|
|
1505
|
-
`${isUpdate ? 'Updated' : 'Created'} path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`
|
|
1506
|
-
);
|
|
1385
|
+
console.log(`✅ saveSteps: Successfully saved all ${this.spec.Steps.length} step(s)`);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Save all step paths for this agent (Flow agents only).
|
|
1390
|
+
*
|
|
1391
|
+
* Creates AIAgentStepPath records that define the edges in a Flow agent's
|
|
1392
|
+
* execution graph, including conditional branching logic.
|
|
1393
|
+
*
|
|
1394
|
+
* @private
|
|
1395
|
+
* @param agentId - The parent agent ID
|
|
1396
|
+
* @throws {Error} If any path save fails
|
|
1397
|
+
*/
|
|
1398
|
+
private async saveStepPaths(agentId: string): Promise<void> {
|
|
1399
|
+
console.log(`🔶 saveStepPaths: Called with agentId=${agentId}, Paths=${this.spec.Paths ? this.spec.Paths.length : 'undefined'}`);
|
|
1400
|
+
|
|
1401
|
+
if (!this.spec.Paths || this.spec.Paths.length === 0) {
|
|
1402
|
+
console.log('🔶 saveStepPaths: No paths to save, returning early');
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1507
1405
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1406
|
+
console.log(`🔶 saveStepPaths: Processing ${this.spec.Paths.length} path(s)...`);
|
|
1407
|
+
const md = new Metadata();
|
|
1510
1408
|
|
|
1511
|
-
|
|
1409
|
+
// Create a map of step names to IDs for easy lookup
|
|
1410
|
+
const stepNameToId = new Map<string, string>();
|
|
1411
|
+
if (this.spec.Steps) {
|
|
1412
|
+
for (const step of this.spec.Steps) {
|
|
1413
|
+
if (step.ID) {
|
|
1414
|
+
stepNameToId.set(step.Name, step.ID);
|
|
1512
1415
|
}
|
|
1513
|
-
|
|
1514
|
-
console.log(`✅ saveStepPaths: Successfully saved all ${this.spec.Paths.length} path(s)`);
|
|
1416
|
+
}
|
|
1515
1417
|
}
|
|
1516
1418
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1419
|
+
for (const pathSpec of this.spec.Paths) {
|
|
1420
|
+
const pathEntity = await md.GetEntityObject<AIAgentStepPathEntity>('MJ: AI Agent Step Paths', this._contextUser);
|
|
1421
|
+
|
|
1422
|
+
// Load existing if ID provided
|
|
1423
|
+
const isUpdate = !!(pathSpec.ID && pathSpec.ID !== '');
|
|
1424
|
+
if (isUpdate) {
|
|
1425
|
+
await pathEntity.Load(pathSpec.ID);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Resolve step IDs from step names
|
|
1429
|
+
// OriginStepID and DestinationStepID can be either step IDs or step names
|
|
1430
|
+
// If they're names, look them up in our map
|
|
1431
|
+
let originStepId = pathSpec.OriginStepID;
|
|
1432
|
+
let destinationStepId = pathSpec.DestinationStepID;
|
|
1433
|
+
|
|
1434
|
+
// Try to resolve as step names first
|
|
1435
|
+
if (stepNameToId.has(pathSpec.OriginStepID)) {
|
|
1436
|
+
originStepId = stepNameToId.get(pathSpec.OriginStepID)!;
|
|
1437
|
+
}
|
|
1438
|
+
if (stepNameToId.has(pathSpec.DestinationStepID)) {
|
|
1439
|
+
destinationStepId = stepNameToId.get(pathSpec.DestinationStepID)!;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Map fields
|
|
1443
|
+
pathEntity.OriginStepID = originStepId;
|
|
1444
|
+
pathEntity.DestinationStepID = destinationStepId;
|
|
1445
|
+
pathEntity.Condition = pathSpec.Condition || null;
|
|
1446
|
+
pathEntity.Description = pathSpec.Description || null;
|
|
1447
|
+
pathEntity.Priority = pathSpec.Priority;
|
|
1448
|
+
|
|
1449
|
+
const saved = await pathEntity.Save();
|
|
1450
|
+
if (!saved) {
|
|
1451
|
+
throw new Error(`Failed to save path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Track mutation
|
|
1455
|
+
this.trackMutation(
|
|
1456
|
+
'AI Agent Step Paths',
|
|
1457
|
+
isUpdate ? 'Update' : 'Create',
|
|
1458
|
+
pathEntity.ID,
|
|
1459
|
+
`${isUpdate ? 'Updated' : 'Created'} path from "${pathSpec.OriginStepID}" to "${pathSpec.DestinationStepID}"`
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
// Update the spec with the saved ID
|
|
1463
|
+
pathSpec.ID = pathEntity.ID;
|
|
1464
|
+
|
|
1465
|
+
console.log(`✅ saveStepPaths: Created AIAgentStepPath with ID: ${pathEntity.ID}`);
|
|
1537
1466
|
}
|
|
1538
1467
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
ParentID: partial.ParentID,
|
|
1562
|
-
DriverClass: partial.DriverClass,
|
|
1563
|
-
ModelSelectionMode: partial.ModelSelectionMode || 'Agent Type',
|
|
1564
|
-
PayloadDownstreamPaths: partial.PayloadDownstreamPaths,
|
|
1565
|
-
PayloadUpstreamPaths: partial.PayloadUpstreamPaths,
|
|
1566
|
-
PayloadSelfReadPaths: partial.PayloadSelfReadPaths,
|
|
1567
|
-
PayloadSelfWritePaths: partial.PayloadSelfWritePaths,
|
|
1568
|
-
PayloadScope: partial.PayloadScope,
|
|
1569
|
-
FinalPayloadValidation: partial.FinalPayloadValidation,
|
|
1570
|
-
FinalPayloadValidationMode: partial.FinalPayloadValidationMode || 'Retry',
|
|
1571
|
-
FinalPayloadValidationMaxRetries: partial.FinalPayloadValidationMaxRetries,
|
|
1572
|
-
MaxCostPerRun: partial.MaxCostPerRun,
|
|
1573
|
-
MaxTokensPerRun: partial.MaxTokensPerRun,
|
|
1574
|
-
MaxIterationsPerRun: partial.MaxIterationsPerRun,
|
|
1575
|
-
MaxTimePerRun: partial.MaxTimePerRun,
|
|
1576
|
-
MinExecutionsPerRun: partial.MinExecutionsPerRun,
|
|
1577
|
-
MaxExecutionsPerRun: partial.MaxExecutionsPerRun,
|
|
1578
|
-
StartingPayloadValidation: partial.StartingPayloadValidation,
|
|
1579
|
-
StartingPayloadValidationMode: partial.StartingPayloadValidationMode || 'Fail',
|
|
1580
|
-
DefaultPromptEffortLevel: partial.DefaultPromptEffortLevel,
|
|
1581
|
-
ChatHandlingOption: partial.ChatHandlingOption,
|
|
1582
|
-
DefaultArtifactTypeID: partial.DefaultArtifactTypeID,
|
|
1583
|
-
OwnerUserID: partial.OwnerUserID,
|
|
1584
|
-
InvocationMode: partial.InvocationMode || 'Any',
|
|
1585
|
-
FunctionalRequirements: partial.FunctionalRequirements,
|
|
1586
|
-
TechnicalDesign: partial.TechnicalDesign,
|
|
1587
|
-
Actions: partial.Actions || [],
|
|
1588
|
-
SubAgents: partial.SubAgents || [],
|
|
1589
|
-
Prompts: partial.Prompts || [],
|
|
1590
|
-
|
|
1591
|
-
// Flow agent fields - critical for Flow agent support
|
|
1592
|
-
Steps: partial.Steps || [],
|
|
1593
|
-
Paths: partial.Paths || []
|
|
1594
|
-
};
|
|
1468
|
+
console.log(`✅ saveStepPaths: Successfully saved all ${this.spec.Paths.length} path(s)`);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// ===== UTILITY METHODS =====
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Parse a JSON string field, returning undefined if null/empty.
|
|
1475
|
+
*
|
|
1476
|
+
* Safely parses JSON fields from the database, handling null/undefined values
|
|
1477
|
+
* and logging errors if parsing fails.
|
|
1478
|
+
*
|
|
1479
|
+
* @private
|
|
1480
|
+
* @param jsonString - The JSON string to parse
|
|
1481
|
+
* @returns Parsed object or undefined if null/empty/invalid
|
|
1482
|
+
*/
|
|
1483
|
+
private parseJsonField<T>(jsonString: string | null | undefined): T | undefined {
|
|
1484
|
+
if (!jsonString) return undefined;
|
|
1485
|
+
try {
|
|
1486
|
+
return JSON.parse(jsonString) as T;
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
LogError(`Failed to parse JSON field: ${error}`);
|
|
1489
|
+
return undefined;
|
|
1595
1490
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Initialize a spec with defaults for any missing required fields.
|
|
1495
|
+
*
|
|
1496
|
+
* Takes a partial spec and fills in defaults for any missing fields to ensure
|
|
1497
|
+
* a valid AgentSpec structure.
|
|
1498
|
+
*
|
|
1499
|
+
* @private
|
|
1500
|
+
* @param partial - Partial agent specification
|
|
1501
|
+
* @returns Complete AgentSpec with defaults
|
|
1502
|
+
*/
|
|
1503
|
+
private initializeSpec(partial: Partial<AgentSpec>): AgentSpec {
|
|
1504
|
+
return {
|
|
1505
|
+
ID: partial.ID || '',
|
|
1506
|
+
Name: partial.Name || '',
|
|
1507
|
+
Description: partial.Description,
|
|
1508
|
+
|
|
1509
|
+
// TypeID and Status are extended fields (not in base AgentSpec interface)
|
|
1510
|
+
TypeID: (partial as any).TypeID,
|
|
1511
|
+
Status: (partial as any).Status || 'Active',
|
|
1512
|
+
|
|
1513
|
+
IconClass: partial.IconClass,
|
|
1514
|
+
LogoURL: partial.LogoURL,
|
|
1515
|
+
ParentID: partial.ParentID,
|
|
1516
|
+
DriverClass: partial.DriverClass,
|
|
1517
|
+
ModelSelectionMode: partial.ModelSelectionMode || 'Agent Type',
|
|
1518
|
+
PayloadDownstreamPaths: partial.PayloadDownstreamPaths,
|
|
1519
|
+
PayloadUpstreamPaths: partial.PayloadUpstreamPaths,
|
|
1520
|
+
PayloadSelfReadPaths: partial.PayloadSelfReadPaths,
|
|
1521
|
+
PayloadSelfWritePaths: partial.PayloadSelfWritePaths,
|
|
1522
|
+
PayloadScope: partial.PayloadScope,
|
|
1523
|
+
FinalPayloadValidation: partial.FinalPayloadValidation,
|
|
1524
|
+
FinalPayloadValidationMode: partial.FinalPayloadValidationMode || 'Retry',
|
|
1525
|
+
FinalPayloadValidationMaxRetries: partial.FinalPayloadValidationMaxRetries,
|
|
1526
|
+
MaxCostPerRun: partial.MaxCostPerRun,
|
|
1527
|
+
MaxTokensPerRun: partial.MaxTokensPerRun,
|
|
1528
|
+
MaxIterationsPerRun: partial.MaxIterationsPerRun,
|
|
1529
|
+
MaxTimePerRun: partial.MaxTimePerRun,
|
|
1530
|
+
MinExecutionsPerRun: partial.MinExecutionsPerRun,
|
|
1531
|
+
MaxExecutionsPerRun: partial.MaxExecutionsPerRun,
|
|
1532
|
+
StartingPayloadValidation: partial.StartingPayloadValidation,
|
|
1533
|
+
StartingPayloadValidationMode: partial.StartingPayloadValidationMode || 'Fail',
|
|
1534
|
+
DefaultPromptEffortLevel: partial.DefaultPromptEffortLevel,
|
|
1535
|
+
ChatHandlingOption: partial.ChatHandlingOption,
|
|
1536
|
+
DefaultArtifactTypeID: partial.DefaultArtifactTypeID,
|
|
1537
|
+
OwnerUserID: partial.OwnerUserID,
|
|
1538
|
+
InvocationMode: partial.InvocationMode || 'Any',
|
|
1539
|
+
FunctionalRequirements: partial.FunctionalRequirements,
|
|
1540
|
+
TechnicalDesign: partial.TechnicalDesign,
|
|
1541
|
+
Actions: partial.Actions || [],
|
|
1542
|
+
SubAgents: partial.SubAgents || [],
|
|
1543
|
+
Prompts: partial.Prompts || [],
|
|
1544
|
+
|
|
1545
|
+
// Flow agent fields - critical for Flow agent support
|
|
1546
|
+
Steps: partial.Steps || [],
|
|
1547
|
+
Paths: partial.Paths || [],
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Get a clean serializable version of the spec.
|
|
1553
|
+
*
|
|
1554
|
+
* Returns a plain JavaScript object suitable for JSON serialization,
|
|
1555
|
+
* API responses, or storage. This is useful when you need to send the
|
|
1556
|
+
* agent spec over the wire or store it in a file.
|
|
1557
|
+
*
|
|
1558
|
+
* @returns Clean copy of the agent specification
|
|
1559
|
+
*
|
|
1560
|
+
* @example
|
|
1561
|
+
* ```typescript
|
|
1562
|
+
* const spec = await AgentSpecSync.LoadFromDatabase('agent-uuid', contextUser);
|
|
1563
|
+
* const json = spec.toJSON();
|
|
1564
|
+
* res.json(json); // Send as API response
|
|
1565
|
+
* ```
|
|
1566
|
+
*/
|
|
1567
|
+
public toJSON(): AgentSpec {
|
|
1568
|
+
return { ...this.spec };
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Check if this spec has unsaved changes.
|
|
1573
|
+
*
|
|
1574
|
+
* @returns True if there are unsaved changes, false otherwise
|
|
1575
|
+
*/
|
|
1576
|
+
public get isDirty(): boolean {
|
|
1577
|
+
return this._isDirty;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Check if this spec has been loaded from the database.
|
|
1582
|
+
*
|
|
1583
|
+
* @returns True if loaded from database, false if created in memory
|
|
1584
|
+
*/
|
|
1585
|
+
public get isLoaded(): boolean {
|
|
1586
|
+
return this._isLoaded;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Mark the spec as having changes.
|
|
1591
|
+
*
|
|
1592
|
+
* Call this method after modifying the spec to indicate that changes need to be saved.
|
|
1593
|
+
* The spec is automatically marked dirty when created with the constructor, but if you
|
|
1594
|
+
* load a spec and then modify it, you should call this method.
|
|
1595
|
+
*
|
|
1596
|
+
* @example
|
|
1597
|
+
* ```typescript
|
|
1598
|
+
* const spec = await AgentSpecSync.LoadFromDatabase('agent-uuid', contextUser);
|
|
1599
|
+
* spec.spec.Description = 'New description';
|
|
1600
|
+
* spec.markDirty();
|
|
1601
|
+
* await spec.SaveToDatabase();
|
|
1602
|
+
* ```
|
|
1603
|
+
*/
|
|
1604
|
+
public markDirty(): void {
|
|
1605
|
+
this._isDirty = true;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Mark the spec as having been loaded from the database.
|
|
1610
|
+
*
|
|
1611
|
+
* This is used internally when updating existing child agents to ensure the delete
|
|
1612
|
+
* logic runs correctly. When a child agent is updated via saveChildSubAgent(), we
|
|
1613
|
+
* create a new AgentSpecSync instance from the spec data (not loaded from DB), but
|
|
1614
|
+
* we need to mark it as loaded so orphaned records are properly deleted.
|
|
1615
|
+
*
|
|
1616
|
+
* @internal
|
|
1617
|
+
* @example
|
|
1618
|
+
* ```typescript
|
|
1619
|
+
* const childSync = new AgentSpecSync(existingChildSpec, contextUser);
|
|
1620
|
+
* childSync.markDirty();
|
|
1621
|
+
* childSync.markLoaded(); // Ensure delete logic runs for existing agent
|
|
1622
|
+
* await childSync.SaveToDatabase();
|
|
1623
|
+
* ```
|
|
1624
|
+
*/
|
|
1625
|
+
public markLoaded(): void {
|
|
1626
|
+
this._isLoaded = true;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// ===== DELETE/ORPHAN METHODS =====
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Load the current state of all agent-related records from the database.
|
|
1633
|
+
*
|
|
1634
|
+
* This method efficiently batches all queries using RunViews for optimal performance.
|
|
1635
|
+
* It loads all junction records and child agents for the specified agent.
|
|
1636
|
+
*
|
|
1637
|
+
* @private
|
|
1638
|
+
* @param agentId - The agent ID to load database state for
|
|
1639
|
+
* @returns Promise resolving to DatabaseState with all current records
|
|
1640
|
+
* @throws {Error} If any query fails
|
|
1641
|
+
*/
|
|
1642
|
+
private async loadCurrentDatabaseState(agentId: string): Promise<DatabaseState> {
|
|
1643
|
+
const rv = new RunView();
|
|
1644
|
+
|
|
1645
|
+
// Batch load all related records for this agent
|
|
1646
|
+
const [actions, prompts, relationships, steps, childAgents] = await rv.RunViews(
|
|
1647
|
+
[
|
|
1648
|
+
{
|
|
1649
|
+
EntityName: 'AI Agent Actions',
|
|
1650
|
+
ExtraFilter: `AgentID='${agentId}'`,
|
|
1651
|
+
ResultType: 'entity_object',
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
EntityName: 'MJ: AI Agent Prompts',
|
|
1655
|
+
ExtraFilter: `AgentID='${agentId}'`,
|
|
1656
|
+
ResultType: 'entity_object',
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
EntityName: 'MJ: AI Agent Relationships',
|
|
1660
|
+
ExtraFilter: `AgentID='${agentId}'`,
|
|
1661
|
+
ResultType: 'entity_object',
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
EntityName: 'MJ: AI Agent Steps',
|
|
1665
|
+
ExtraFilter: `AgentID='${agentId}'`,
|
|
1666
|
+
ResultType: 'entity_object',
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
EntityName: 'AI Agents',
|
|
1670
|
+
ExtraFilter: `ParentID='${agentId}'`,
|
|
1671
|
+
ResultType: 'entity_object',
|
|
1672
|
+
},
|
|
1673
|
+
],
|
|
1674
|
+
this._contextUser
|
|
1675
|
+
);
|
|
1676
|
+
|
|
1677
|
+
// Check for errors
|
|
1678
|
+
if (!actions.Success || !prompts.Success || !relationships.Success || !steps.Success || !childAgents.Success) {
|
|
1679
|
+
const errors = [actions, prompts, relationships, steps, childAgents]
|
|
1680
|
+
.filter((r) => !r.Success)
|
|
1681
|
+
.map((r) => r.ErrorMessage)
|
|
1682
|
+
.join('; ');
|
|
1683
|
+
throw new Error(`Failed to load database state: ${errors}`);
|
|
1615
1684
|
}
|
|
1616
1685
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1686
|
+
// Load paths separately after we have steps (to avoid subquery in ExtraFilter)
|
|
1687
|
+
let pathsResult;
|
|
1688
|
+
const stepRecords = steps.Results || [];
|
|
1689
|
+
if (stepRecords.length > 0) {
|
|
1690
|
+
const stepIds = stepRecords.map((s: AIAgentStepEntity) => `'${s.ID}'`).join(',');
|
|
1691
|
+
pathsResult = await rv.RunView<AIAgentStepPathEntity>(
|
|
1692
|
+
{
|
|
1693
|
+
EntityName: 'MJ: AI Agent Step Paths',
|
|
1694
|
+
ExtraFilter: `OriginStepID IN (${stepIds})`,
|
|
1695
|
+
ResultType: 'entity_object',
|
|
1696
|
+
},
|
|
1697
|
+
this._contextUser
|
|
1698
|
+
);
|
|
1699
|
+
|
|
1700
|
+
if (!pathsResult.Success) {
|
|
1701
|
+
throw new Error(`Failed to load step paths: ${pathsResult.ErrorMessage}`);
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
// No steps, so no paths
|
|
1705
|
+
pathsResult = { Success: true, Results: [] };
|
|
1624
1706
|
}
|
|
1625
1707
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1708
|
+
return {
|
|
1709
|
+
actions: actions.Results || [],
|
|
1710
|
+
prompts: prompts.Results || [],
|
|
1711
|
+
relationships: relationships.Results || [],
|
|
1712
|
+
steps: stepRecords,
|
|
1713
|
+
paths: pathsResult.Results || [],
|
|
1714
|
+
childAgents: childAgents.Results || [],
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Identify orphaned records by comparing database state to AgentSpec.
|
|
1720
|
+
*
|
|
1721
|
+
* An "orphan" is a database record that exists but is not represented in the
|
|
1722
|
+
* current AgentSpec. These records need to be deleted or orphaned.
|
|
1723
|
+
*
|
|
1724
|
+
* The diff logic matches by primary key ID:
|
|
1725
|
+
* - If a DB record's ID is found in the spec → KEEP (may be updated)
|
|
1726
|
+
* - If a DB record's ID is NOT in the spec → ORPHAN/DELETE
|
|
1727
|
+
*
|
|
1728
|
+
* @private
|
|
1729
|
+
* @param dbState - Current database state loaded from loadCurrentDatabaseState
|
|
1730
|
+
* @param spec - The new AgentSpec to compare against
|
|
1731
|
+
* @returns Orphans object containing all orphaned records
|
|
1732
|
+
*/
|
|
1733
|
+
private identifyOrphans(dbState: DatabaseState, spec: AgentSpec): Orphans {
|
|
1734
|
+
return {
|
|
1735
|
+
// Orphaned actions: DB actions not in spec.Actions
|
|
1736
|
+
actions: dbState.actions.filter((dbAction) => !spec.Actions?.some((specAction) => specAction.AgentActionID === dbAction.ID)),
|
|
1737
|
+
|
|
1738
|
+
// Orphaned prompt junctions: DB prompts not in spec.Prompts
|
|
1739
|
+
prompts: dbState.prompts.filter((dbPrompt) => !spec.Prompts?.some((specPrompt) => (specPrompt as any).ID === dbPrompt.ID)),
|
|
1740
|
+
|
|
1741
|
+
// Orphaned relationships: DB relationships not in spec.SubAgents (related type)
|
|
1742
|
+
relationships: dbState.relationships.filter(
|
|
1743
|
+
(dbRel) => !spec.SubAgents?.some((specSub) => specSub.Type === 'related' && specSub.AgentRelationshipID === dbRel.ID)
|
|
1744
|
+
),
|
|
1745
|
+
|
|
1746
|
+
// Orphaned steps: DB steps not in spec.Steps
|
|
1747
|
+
steps: dbState.steps.filter((dbStep) => !spec.Steps?.some((specStep) => specStep.ID === dbStep.ID)),
|
|
1748
|
+
|
|
1749
|
+
// Orphaned paths: DB paths not in spec.Paths
|
|
1750
|
+
paths: dbState.paths.filter((dbPath) => !spec.Paths?.some((specPath) => specPath.ID === dbPath.ID)),
|
|
1751
|
+
|
|
1752
|
+
// Orphaned child agents: DB children not in spec.SubAgents (child type)
|
|
1753
|
+
childAgents: dbState.childAgents.filter(
|
|
1754
|
+
(dbChild) => !spec.SubAgents?.some((specSub) => specSub.Type === 'child' && specSub.SubAgent.ID === dbChild.ID)
|
|
1755
|
+
),
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Delete or orphan all orphaned records in the correct order.
|
|
1761
|
+
*
|
|
1762
|
+
* This method executes deletions in phases to maintain referential integrity:
|
|
1763
|
+
* - Phase 1: Leaf entities (paths, actions) - no other entities reference these
|
|
1764
|
+
* - Phase 2: Mid-level entities (steps, prompt junctions, relationships)
|
|
1765
|
+
* - Phase 3: Child agents (orphan by setting ParentID = NULL)
|
|
1766
|
+
*
|
|
1767
|
+
* IMPORTANT: We NEVER delete AIPrompt entities themselves, only AIAgentPrompt junctions.
|
|
1768
|
+
* IMPORTANT: We ORPHAN child agents instead of deleting them to avoid foreign key violations from run history.
|
|
1769
|
+
*
|
|
1770
|
+
* @private
|
|
1771
|
+
* @param orphans - The orphaned records identified by identifyOrphans
|
|
1772
|
+
* @throws {Error} If any delete operation fails
|
|
1773
|
+
*/
|
|
1774
|
+
private async deleteOrphans(orphans: Orphans): Promise<void> {
|
|
1775
|
+
const md = new Metadata();
|
|
1776
|
+
|
|
1777
|
+
// Phase 1: Leaf entities (Paths, Actions)
|
|
1778
|
+
console.log(`🗑️ Phase 1: Deleting ${orphans.paths.length} paths and ${orphans.actions.length} actions...`);
|
|
1779
|
+
|
|
1780
|
+
for (const path of orphans.paths) {
|
|
1781
|
+
console.log(`🗑️ deleteOrphans: Deleting path ${path.ID}...`);
|
|
1782
|
+
const entity = await md.GetEntityObject<AIAgentStepPathEntity>('MJ: AI Agent Step Paths', this._contextUser);
|
|
1783
|
+
const loaded = await entity.Load(path.ID);
|
|
1784
|
+
if (!loaded) {
|
|
1785
|
+
console.error(`❌ deleteOrphans: Failed to load path ${path.ID} - skipping`);
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
const deleted = await entity.Delete();
|
|
1789
|
+
if (!deleted) {
|
|
1790
|
+
console.error(`❌ deleteOrphans: Failed to delete path ${path.ID}`);
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
console.log(`✅ deleteOrphans: Deleted path ${path.ID}`);
|
|
1794
|
+
this.trackMutation('AI Agent Step Paths', 'Delete', path.ID, `Deleted orphaned path`);
|
|
1633
1795
|
}
|
|
1634
1796
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
public markDirty(): void {
|
|
1651
|
-
this._isDirty = true;
|
|
1797
|
+
for (const action of orphans.actions) {
|
|
1798
|
+
console.log(`🗑️ deleteOrphans: Deleting action junction ${action.ID} (ActionID: ${action.ActionID})...`);
|
|
1799
|
+
const entity = await md.GetEntityObject<AIAgentActionEntity>('AI Agent Actions', this._contextUser);
|
|
1800
|
+
const loaded = await entity.Load(action.ID);
|
|
1801
|
+
if (!loaded) {
|
|
1802
|
+
console.error(`❌ deleteOrphans: Failed to load action junction ${action.ID} - skipping`);
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
const deleted = await entity.Delete();
|
|
1806
|
+
if (!deleted) {
|
|
1807
|
+
console.error(`❌ deleteOrphans: Failed to delete action junction ${action.ID}`);
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
console.log(`✅ deleteOrphans: Deleted action junction ${action.ID}`);
|
|
1811
|
+
this.trackMutation('AI Agent Actions', 'Delete', action.ID, `Deleted orphaned action junction`);
|
|
1652
1812
|
}
|
|
1653
1813
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1814
|
+
// Phase 2: Mid-level entities (Steps, Prompt Junctions, Relationships)
|
|
1815
|
+
console.log(
|
|
1816
|
+
`🗑️ Phase 2: Deleting ${orphans.steps.length} steps, ${orphans.prompts.length} prompt junctions, ${orphans.relationships.length} relationships...`
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
for (const step of orphans.steps) {
|
|
1820
|
+
console.log(`🗑️ deleteOrphans: Deleting step ${step.ID} (${step.Name})...`);
|
|
1821
|
+
const entity = await md.GetEntityObject<AIAgentStepEntity>('MJ: AI Agent Steps', this._contextUser);
|
|
1822
|
+
const loaded = await entity.Load(step.ID);
|
|
1823
|
+
if (!loaded) {
|
|
1824
|
+
console.error(`❌ deleteOrphans: Failed to load step ${step.ID} - skipping`);
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
const deleted = await entity.Delete();
|
|
1828
|
+
if (!deleted) {
|
|
1829
|
+
console.error(`❌ deleteOrphans: Failed to delete step ${step.ID}`);
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
console.log(`✅ deleteOrphans: Deleted step ${step.ID}`);
|
|
1833
|
+
this.trackMutation('AI Agent Steps', 'Delete', step.ID, `Deleted orphaned step`);
|
|
1673
1834
|
}
|
|
1674
1835
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
// Batch load all related records for this agent
|
|
1692
|
-
const [actions, prompts, relationships, steps, childAgents] = await rv.RunViews([
|
|
1693
|
-
{
|
|
1694
|
-
EntityName: 'AI Agent Actions',
|
|
1695
|
-
ExtraFilter: `AgentID='${agentId}'`,
|
|
1696
|
-
ResultType: 'entity_object'
|
|
1697
|
-
},
|
|
1698
|
-
{
|
|
1699
|
-
EntityName: 'MJ: AI Agent Prompts',
|
|
1700
|
-
ExtraFilter: `AgentID='${agentId}'`,
|
|
1701
|
-
ResultType: 'entity_object'
|
|
1702
|
-
},
|
|
1703
|
-
{
|
|
1704
|
-
EntityName: 'MJ: AI Agent Relationships',
|
|
1705
|
-
ExtraFilter: `AgentID='${agentId}'`,
|
|
1706
|
-
ResultType: 'entity_object'
|
|
1707
|
-
},
|
|
1708
|
-
{
|
|
1709
|
-
EntityName: 'MJ: AI Agent Steps',
|
|
1710
|
-
ExtraFilter: `AgentID='${agentId}'`,
|
|
1711
|
-
ResultType: 'entity_object'
|
|
1712
|
-
},
|
|
1713
|
-
{
|
|
1714
|
-
EntityName: 'AI Agents',
|
|
1715
|
-
ExtraFilter: `ParentID='${agentId}'`,
|
|
1716
|
-
ResultType: 'entity_object'
|
|
1717
|
-
}
|
|
1718
|
-
], this._contextUser);
|
|
1719
|
-
|
|
1720
|
-
// Check for errors
|
|
1721
|
-
if (!actions.Success || !prompts.Success || !relationships.Success ||
|
|
1722
|
-
!steps.Success || !childAgents.Success) {
|
|
1723
|
-
const errors = [actions, prompts, relationships, steps, childAgents]
|
|
1724
|
-
.filter(r => !r.Success)
|
|
1725
|
-
.map(r => r.ErrorMessage)
|
|
1726
|
-
.join('; ');
|
|
1727
|
-
throw new Error(`Failed to load database state: ${errors}`);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
// Load paths separately after we have steps (to avoid subquery in ExtraFilter)
|
|
1731
|
-
let pathsResult;
|
|
1732
|
-
const stepRecords = steps.Results || [];
|
|
1733
|
-
if (stepRecords.length > 0) {
|
|
1734
|
-
const stepIds = stepRecords.map((s: AIAgentStepEntity) => `'${s.ID}'`).join(',');
|
|
1735
|
-
pathsResult = await rv.RunView<AIAgentStepPathEntity>({
|
|
1736
|
-
EntityName: 'MJ: AI Agent Step Paths',
|
|
1737
|
-
ExtraFilter: `OriginStepID IN (${stepIds})`,
|
|
1738
|
-
ResultType: 'entity_object'
|
|
1739
|
-
}, this._contextUser);
|
|
1740
|
-
|
|
1741
|
-
if (!pathsResult.Success) {
|
|
1742
|
-
throw new Error(`Failed to load step paths: ${pathsResult.ErrorMessage}`);
|
|
1743
|
-
}
|
|
1744
|
-
} else {
|
|
1745
|
-
// No steps, so no paths
|
|
1746
|
-
pathsResult = { Success: true, Results: [] };
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
return {
|
|
1750
|
-
actions: actions.Results || [],
|
|
1751
|
-
prompts: prompts.Results || [],
|
|
1752
|
-
relationships: relationships.Results || [],
|
|
1753
|
-
steps: stepRecords,
|
|
1754
|
-
paths: pathsResult.Results || [],
|
|
1755
|
-
childAgents: childAgents.Results || []
|
|
1756
|
-
};
|
|
1836
|
+
for (const promptJunction of orphans.prompts) {
|
|
1837
|
+
console.log(`🗑️ deleteOrphans: Deleting prompt junction ${promptJunction.ID} (PromptID: ${promptJunction.PromptID})...`);
|
|
1838
|
+
const entity = await md.GetEntityObject<any>('MJ: AI Agent Prompts', this._contextUser);
|
|
1839
|
+
const loaded = await entity.Load(promptJunction.ID);
|
|
1840
|
+
if (!loaded) {
|
|
1841
|
+
console.error(`❌ deleteOrphans: Failed to load prompt junction ${promptJunction.ID} - skipping`);
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
const deleted = await entity.Delete();
|
|
1845
|
+
if (!deleted) {
|
|
1846
|
+
console.error(`❌ deleteOrphans: Failed to delete prompt junction ${promptJunction.ID}`);
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
console.log(`✅ deleteOrphans: Deleted prompt junction ${promptJunction.ID}`);
|
|
1850
|
+
this.trackMutation('AI Agent Prompts', 'Delete', promptJunction.ID, `Deleted orphaned prompt junction`);
|
|
1851
|
+
// NOTE: We do NOT delete the AIPrompt itself - it's a shared resource
|
|
1757
1852
|
}
|
|
1758
1853
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
private identifyOrphans(dbState: DatabaseState, spec: AgentSpec): Orphans {
|
|
1775
|
-
return {
|
|
1776
|
-
// Orphaned actions: DB actions not in spec.Actions
|
|
1777
|
-
actions: dbState.actions.filter(dbAction =>
|
|
1778
|
-
!spec.Actions?.some(specAction => specAction.AgentActionID === dbAction.ID)
|
|
1779
|
-
),
|
|
1780
|
-
|
|
1781
|
-
// Orphaned prompt junctions: DB prompts not in spec.Prompts
|
|
1782
|
-
prompts: dbState.prompts.filter(dbPrompt =>
|
|
1783
|
-
!spec.Prompts?.some(specPrompt => (specPrompt as any).ID === dbPrompt.ID)
|
|
1784
|
-
),
|
|
1785
|
-
|
|
1786
|
-
// Orphaned relationships: DB relationships not in spec.SubAgents (related type)
|
|
1787
|
-
relationships: dbState.relationships.filter(dbRel =>
|
|
1788
|
-
!spec.SubAgents?.some(specSub =>
|
|
1789
|
-
specSub.Type === 'related' && specSub.AgentRelationshipID === dbRel.ID
|
|
1790
|
-
)
|
|
1791
|
-
),
|
|
1792
|
-
|
|
1793
|
-
// Orphaned steps: DB steps not in spec.Steps
|
|
1794
|
-
steps: dbState.steps.filter(dbStep =>
|
|
1795
|
-
!spec.Steps?.some(specStep => specStep.ID === dbStep.ID)
|
|
1796
|
-
),
|
|
1797
|
-
|
|
1798
|
-
// Orphaned paths: DB paths not in spec.Paths
|
|
1799
|
-
paths: dbState.paths.filter(dbPath =>
|
|
1800
|
-
!spec.Paths?.some(specPath => specPath.ID === dbPath.ID)
|
|
1801
|
-
),
|
|
1802
|
-
|
|
1803
|
-
// Orphaned child agents: DB children not in spec.SubAgents (child type)
|
|
1804
|
-
childAgents: dbState.childAgents.filter(dbChild =>
|
|
1805
|
-
!spec.SubAgents?.some(specSub =>
|
|
1806
|
-
specSub.Type === 'child' && specSub.SubAgent.ID === dbChild.ID
|
|
1807
|
-
)
|
|
1808
|
-
)
|
|
1809
|
-
};
|
|
1854
|
+
for (const relationship of orphans.relationships) {
|
|
1855
|
+
console.log(`🗑️ deleteOrphans: Deleting relationship ${relationship.ID} (SubAgentID: ${relationship.SubAgentID})...`);
|
|
1856
|
+
const entity = await md.GetEntityObject<AIAgentRelationshipEntity>('MJ: AI Agent Relationships', this._contextUser);
|
|
1857
|
+
const loaded = await entity.Load(relationship.ID);
|
|
1858
|
+
if (!loaded) {
|
|
1859
|
+
console.error(`❌ deleteOrphans: Failed to load relationship ${relationship.ID} - skipping`);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const deleted = await entity.Delete();
|
|
1863
|
+
if (!deleted) {
|
|
1864
|
+
console.error(`❌ deleteOrphans: Failed to delete relationship ${relationship.ID}`);
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
console.log(`✅ deleteOrphans: Deleted relationship ${relationship.ID}`);
|
|
1868
|
+
this.trackMutation('AI Agent Relationships', 'Delete', relationship.ID, `Deleted orphaned relationship`);
|
|
1810
1869
|
}
|
|
1811
1870
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
*
|
|
1815
|
-
* This method executes deletions in phases to maintain referential integrity:
|
|
1816
|
-
* - Phase 1: Leaf entities (paths, actions) - no other entities reference these
|
|
1817
|
-
* - Phase 2: Mid-level entities (steps, prompt junctions, relationships)
|
|
1818
|
-
* - Phase 3: Child agents (orphan by setting ParentID = NULL)
|
|
1819
|
-
*
|
|
1820
|
-
* IMPORTANT: We NEVER delete AIPrompt entities themselves, only AIAgentPrompt junctions.
|
|
1821
|
-
* IMPORTANT: We ORPHAN child agents instead of deleting them to avoid foreign key violations from run history.
|
|
1822
|
-
*
|
|
1823
|
-
* @private
|
|
1824
|
-
* @param orphans - The orphaned records identified by identifyOrphans
|
|
1825
|
-
* @throws {Error} If any delete operation fails
|
|
1826
|
-
*/
|
|
1827
|
-
private async deleteOrphans(orphans: Orphans): Promise<void> {
|
|
1828
|
-
const md = new Metadata();
|
|
1829
|
-
|
|
1830
|
-
// Phase 1: Leaf entities (Paths, Actions)
|
|
1831
|
-
console.log(`🗑️ Phase 1: Deleting ${orphans.paths.length} paths and ${orphans.actions.length} actions...`);
|
|
1832
|
-
|
|
1833
|
-
for (const path of orphans.paths) {
|
|
1834
|
-
console.log(`🗑️ deleteOrphans: Deleting path ${path.ID}...`);
|
|
1835
|
-
const entity = await md.GetEntityObject<AIAgentStepPathEntity>(
|
|
1836
|
-
'MJ: AI Agent Step Paths',
|
|
1837
|
-
this._contextUser
|
|
1838
|
-
);
|
|
1839
|
-
const loaded = await entity.Load(path.ID);
|
|
1840
|
-
if (!loaded) {
|
|
1841
|
-
console.error(`❌ deleteOrphans: Failed to load path ${path.ID} - skipping`);
|
|
1842
|
-
continue;
|
|
1843
|
-
}
|
|
1844
|
-
const deleted = await entity.Delete();
|
|
1845
|
-
if (!deleted) {
|
|
1846
|
-
console.error(`❌ deleteOrphans: Failed to delete path ${path.ID}`);
|
|
1847
|
-
continue;
|
|
1848
|
-
}
|
|
1849
|
-
console.log(`✅ deleteOrphans: Deleted path ${path.ID}`);
|
|
1850
|
-
this.trackMutation('AI Agent Step Paths', 'Delete', path.ID, `Deleted orphaned path`);
|
|
1851
|
-
}
|
|
1871
|
+
// Phase 3: Orphan child agents (set ParentID = NULL)
|
|
1872
|
+
console.log(`🔗 Phase 3: Orphaning ${orphans.childAgents.length} child agents (set ParentID = NULL)...`);
|
|
1852
1873
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
if (!loaded) {
|
|
1884
|
-
console.error(`❌ deleteOrphans: Failed to load step ${step.ID} - skipping`);
|
|
1885
|
-
continue;
|
|
1886
|
-
}
|
|
1887
|
-
const deleted = await entity.Delete();
|
|
1888
|
-
if (!deleted) {
|
|
1889
|
-
console.error(`❌ deleteOrphans: Failed to delete step ${step.ID}`);
|
|
1890
|
-
continue;
|
|
1891
|
-
}
|
|
1892
|
-
console.log(`✅ deleteOrphans: Deleted step ${step.ID}`);
|
|
1893
|
-
this.trackMutation('AI Agent Steps', 'Delete', step.ID, `Deleted orphaned step`);
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
for (const promptJunction of orphans.prompts) {
|
|
1897
|
-
console.log(`🗑️ deleteOrphans: Deleting prompt junction ${promptJunction.ID} (PromptID: ${promptJunction.PromptID})...`);
|
|
1898
|
-
const entity = await md.GetEntityObject<any>(
|
|
1899
|
-
'MJ: AI Agent Prompts',
|
|
1900
|
-
this._contextUser
|
|
1901
|
-
);
|
|
1902
|
-
const loaded = await entity.Load(promptJunction.ID);
|
|
1903
|
-
if (!loaded) {
|
|
1904
|
-
console.error(`❌ deleteOrphans: Failed to load prompt junction ${promptJunction.ID} - skipping`);
|
|
1905
|
-
continue;
|
|
1906
|
-
}
|
|
1907
|
-
const deleted = await entity.Delete();
|
|
1908
|
-
if (!deleted) {
|
|
1909
|
-
console.error(`❌ deleteOrphans: Failed to delete prompt junction ${promptJunction.ID}`);
|
|
1910
|
-
continue;
|
|
1911
|
-
}
|
|
1912
|
-
console.log(`✅ deleteOrphans: Deleted prompt junction ${promptJunction.ID}`);
|
|
1913
|
-
this.trackMutation('AI Agent Prompts', 'Delete', promptJunction.ID, `Deleted orphaned prompt junction`);
|
|
1914
|
-
// NOTE: We do NOT delete the AIPrompt itself - it's a shared resource
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
for (const relationship of orphans.relationships) {
|
|
1918
|
-
console.log(`🗑️ deleteOrphans: Deleting relationship ${relationship.ID} (SubAgentID: ${relationship.SubAgentID})...`);
|
|
1919
|
-
const entity = await md.GetEntityObject<AIAgentRelationshipEntity>(
|
|
1920
|
-
'MJ: AI Agent Relationships',
|
|
1921
|
-
this._contextUser
|
|
1922
|
-
);
|
|
1923
|
-
const loaded = await entity.Load(relationship.ID);
|
|
1924
|
-
if (!loaded) {
|
|
1925
|
-
console.error(`❌ deleteOrphans: Failed to load relationship ${relationship.ID} - skipping`);
|
|
1926
|
-
continue;
|
|
1927
|
-
}
|
|
1928
|
-
const deleted = await entity.Delete();
|
|
1929
|
-
if (!deleted) {
|
|
1930
|
-
console.error(`❌ deleteOrphans: Failed to delete relationship ${relationship.ID}`);
|
|
1931
|
-
continue;
|
|
1932
|
-
}
|
|
1933
|
-
console.log(`✅ deleteOrphans: Deleted relationship ${relationship.ID}`);
|
|
1934
|
-
this.trackMutation('AI Agent Relationships', 'Delete', relationship.ID, `Deleted orphaned relationship`);
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
// Phase 3: Orphan child agents (set ParentID = NULL)
|
|
1938
|
-
console.log(`🔗 Phase 3: Orphaning ${orphans.childAgents.length} child agents (set ParentID = NULL)...`);
|
|
1939
|
-
|
|
1940
|
-
for (const childAgent of orphans.childAgents) {
|
|
1941
|
-
await this.orphanChildAgent(childAgent.ID);
|
|
1942
|
-
this.trackMutation('AI Agents', 'Update', childAgent.ID, `Orphaned child agent: ${childAgent.Name} (set ParentID = NULL)`);
|
|
1943
|
-
}
|
|
1874
|
+
for (const childAgent of orphans.childAgents) {
|
|
1875
|
+
await this.orphanChildAgent(childAgent.ID);
|
|
1876
|
+
this.trackMutation('AI Agents', 'Update', childAgent.ID, `Orphaned child agent: ${childAgent.Name} (set ParentID = NULL)`);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* Orphan a child agent by setting its ParentID to NULL.
|
|
1882
|
+
*
|
|
1883
|
+
* This removes the child from the parent's hierarchy without deleting it.
|
|
1884
|
+
* This approach is used instead of full deletion because:
|
|
1885
|
+
* - Child agent may have AIAgentRun records (execution history)
|
|
1886
|
+
* - AIAgentRun.AgentID is NOT NULL with NO CASCADE DELETE
|
|
1887
|
+
* - Deleting child would cause foreign key violation
|
|
1888
|
+
* - Orphaning preserves history and avoids violation
|
|
1889
|
+
* - Child becomes a standalone agent
|
|
1890
|
+
*
|
|
1891
|
+
* @private
|
|
1892
|
+
* @param childAgentId - The ID of the child agent to orphan
|
|
1893
|
+
* @throws {Error} If orphaning fails
|
|
1894
|
+
*/
|
|
1895
|
+
private async orphanChildAgent(childAgentId: string): Promise<void> {
|
|
1896
|
+
console.log(`🔗 orphanChildAgent: Orphaning child agent ${childAgentId}...`);
|
|
1897
|
+
|
|
1898
|
+
const md = new Metadata();
|
|
1899
|
+
const childAgent = await md.GetEntityObject<AIAgentEntity>('AI Agents', this._contextUser);
|
|
1900
|
+
|
|
1901
|
+
const loaded = await childAgent.Load(childAgentId);
|
|
1902
|
+
if (!loaded) {
|
|
1903
|
+
throw new Error(`Failed to load child agent ${childAgentId} for orphaning`);
|
|
1944
1904
|
}
|
|
1945
1905
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
* This approach is used instead of full deletion because:
|
|
1951
|
-
* - Child agent may have AIAgentRun records (execution history)
|
|
1952
|
-
* - AIAgentRun.AgentID is NOT NULL with NO CASCADE DELETE
|
|
1953
|
-
* - Deleting child would cause foreign key violation
|
|
1954
|
-
* - Orphaning preserves history and avoids violation
|
|
1955
|
-
* - Child becomes a standalone agent
|
|
1956
|
-
*
|
|
1957
|
-
* @private
|
|
1958
|
-
* @param childAgentId - The ID of the child agent to orphan
|
|
1959
|
-
* @throws {Error} If orphaning fails
|
|
1960
|
-
*/
|
|
1961
|
-
private async orphanChildAgent(childAgentId: string): Promise<void> {
|
|
1962
|
-
console.log(`🔗 orphanChildAgent: Orphaning child agent ${childAgentId}...`);
|
|
1963
|
-
|
|
1964
|
-
const md = new Metadata();
|
|
1965
|
-
const childAgent = await md.GetEntityObject<AIAgentEntity>('AI Agents', this._contextUser);
|
|
1966
|
-
|
|
1967
|
-
const loaded = await childAgent.Load(childAgentId);
|
|
1968
|
-
if (!loaded) {
|
|
1969
|
-
throw new Error(`Failed to load child agent ${childAgentId} for orphaning`);
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
childAgent.ParentID = null; // Orphan the child
|
|
1973
|
-
const saved = await childAgent.Save();
|
|
1974
|
-
if (!saved) {
|
|
1975
|
-
throw new Error(`Failed to orphan child agent ${childAgentId}`);
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
console.log(`✅ orphanChildAgent: Orphaned child agent ${childAgentId} (ParentID = NULL)`);
|
|
1906
|
+
childAgent.ParentID = null; // Orphan the child
|
|
1907
|
+
const saved = await childAgent.Save();
|
|
1908
|
+
if (!saved) {
|
|
1909
|
+
throw new Error(`Failed to orphan child agent ${childAgentId}`);
|
|
1979
1910
|
}
|
|
1980
|
-
|
|
1911
|
+
|
|
1912
|
+
console.log(`✅ orphanChildAgent: Orphaned child agent ${childAgentId} (ParentID = NULL)`);
|
|
1913
|
+
}
|
|
1914
|
+
}
|