@memberjunction/server 2.103.0 → 2.105.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.
Files changed (93) hide show
  1. package/dist/agents/skip-agent.d.ts +29 -0
  2. package/dist/agents/skip-agent.d.ts.map +1 -0
  3. package/dist/agents/skip-agent.js +1308 -0
  4. package/dist/agents/skip-agent.js.map +1 -0
  5. package/dist/agents/skip-sdk.d.ts +47 -0
  6. package/dist/agents/skip-sdk.d.ts.map +1 -0
  7. package/dist/agents/skip-sdk.js +269 -0
  8. package/dist/agents/skip-sdk.js.map +1 -0
  9. package/dist/config.d.ts +9 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +1 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/generated/generated.d.ts +3660 -3386
  14. package/dist/generated/generated.d.ts.map +1 -1
  15. package/dist/generated/generated.js +22009 -20223
  16. package/dist/generated/generated.js.map +1 -1
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -18
  20. package/dist/index.js.map +1 -1
  21. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  22. package/dist/resolvers/AskSkipResolver.js +24 -9
  23. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  24. package/dist/resolvers/ComponentRegistryResolver.d.ts +19 -0
  25. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  26. package/dist/resolvers/ComponentRegistryResolver.js +140 -2
  27. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  28. package/dist/resolvers/CreateQueryResolver.d.ts +2 -2
  29. package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
  30. package/dist/resolvers/CreateQueryResolver.js +12 -12
  31. package/dist/resolvers/CreateQueryResolver.js.map +1 -1
  32. package/dist/resolvers/EntityResolver.d.ts +2 -2
  33. package/dist/resolvers/EntityResolver.d.ts.map +1 -1
  34. package/dist/resolvers/EntityResolver.js +4 -4
  35. package/dist/resolvers/EntityResolver.js.map +1 -1
  36. package/dist/resolvers/FileCategoryResolver.d.ts +1 -1
  37. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  38. package/dist/resolvers/FileCategoryResolver.js +2 -2
  39. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  40. package/dist/resolvers/FileResolver.d.ts +6 -6
  41. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  42. package/dist/resolvers/FileResolver.js +14 -14
  43. package/dist/resolvers/FileResolver.js.map +1 -1
  44. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
  45. package/dist/resolvers/PotentialDuplicateRecordResolver.js +0 -2
  46. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  47. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -3
  48. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  49. package/dist/resolvers/RunAIAgentResolver.js +28 -21
  50. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  51. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  52. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  53. package/dist/resolvers/TaskResolver.d.ts +18 -0
  54. package/dist/resolvers/TaskResolver.d.ts.map +1 -0
  55. package/dist/resolvers/TaskResolver.js +138 -0
  56. package/dist/resolvers/TaskResolver.js.map +1 -0
  57. package/dist/resolvers/UserFavoriteResolver.d.ts +2 -2
  58. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  59. package/dist/resolvers/UserFavoriteResolver.js +5 -5
  60. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  61. package/dist/resolvers/UserResolver.d.ts +2 -2
  62. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  63. package/dist/resolvers/UserResolver.js +7 -7
  64. package/dist/resolvers/UserResolver.js.map +1 -1
  65. package/dist/resolvers/UserViewResolver.d.ts +2 -2
  66. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  67. package/dist/resolvers/UserViewResolver.js +8 -8
  68. package/dist/resolvers/UserViewResolver.js.map +1 -1
  69. package/dist/services/TaskOrchestrator.d.ts +52 -0
  70. package/dist/services/TaskOrchestrator.d.ts.map +1 -0
  71. package/dist/services/TaskOrchestrator.js +486 -0
  72. package/dist/services/TaskOrchestrator.js.map +1 -0
  73. package/package.json +30 -38
  74. package/src/agents/skip-agent.ts +1433 -0
  75. package/src/agents/skip-sdk.ts +541 -0
  76. package/src/config.ts +3 -2
  77. package/src/generated/generated.ts +7948 -6811
  78. package/src/index.ts +7 -21
  79. package/src/resolvers/AskSkipResolver.ts +32 -10
  80. package/src/resolvers/ComponentRegistryResolver.ts +133 -4
  81. package/src/resolvers/CreateQueryResolver.ts +6 -6
  82. package/src/resolvers/EntityResolver.ts +4 -4
  83. package/src/resolvers/FileCategoryResolver.ts +2 -2
  84. package/src/resolvers/FileResolver.ts +12 -12
  85. package/src/resolvers/PotentialDuplicateRecordResolver.ts +2 -3
  86. package/src/resolvers/RunAIAgentResolver.ts +23 -10
  87. package/src/resolvers/RunTemplateResolver.ts +1 -2
  88. package/src/resolvers/TaskResolver.ts +142 -0
  89. package/src/resolvers/UserFavoriteResolver.ts +5 -5
  90. package/src/resolvers/UserResolver.ts +7 -7
  91. package/src/resolvers/UserViewResolver.ts +8 -8
  92. package/src/services/TaskOrchestration-Integration.md +188 -0
  93. package/src/services/TaskOrchestrator.ts +756 -0
@@ -0,0 +1,756 @@
1
+ import { Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
2
+ import { TaskEntity, TaskDependencyEntity, TaskTypeEntity, AIAgentEntityExtended, ConversationDetailEntity, ArtifactEntity, ArtifactVersionEntity, ConversationDetailArtifactEntity } from '@memberjunction/core-entities';
3
+ import { AgentRunner } from '@memberjunction/ai-agents';
4
+ import { ChatMessageRole } from '@memberjunction/ai';
5
+ import { PubSubEngine } from 'type-graphql';
6
+ import { UserPayload } from '../types.js';
7
+ import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
8
+
9
+ /**
10
+ * Task definition from LLM response
11
+ */
12
+ export interface TaskDefinition {
13
+ tempId: string; // LLM-generated ID for reference
14
+ name: string;
15
+ description: string;
16
+ agentName: string;
17
+ dependsOn: string[]; // Array of tempIds this task depends on
18
+ inputPayload?: any;
19
+ }
20
+
21
+ /**
22
+ * Task graph response from Conversation Manager
23
+ */
24
+ export interface TaskGraphResponse {
25
+ workflowName: string; // Name for the parent/workflow task
26
+ tasks: TaskDefinition[];
27
+ reasoning?: string;
28
+ }
29
+
30
+ /**
31
+ * Task execution result
32
+ */
33
+ export interface TaskExecutionResult {
34
+ taskId: string;
35
+ success: boolean;
36
+ output?: any;
37
+ error?: string;
38
+ }
39
+
40
+ /**
41
+ * TaskOrchestrator handles multi-step task execution with dependencies
42
+ */
43
+ export class TaskOrchestrator {
44
+ // Default artifact type ID for JSON (when agent doesn't specify DefaultArtifactTypeID)
45
+ private readonly JSON_ARTIFACT_TYPE_ID = 'ae674c7e-ea0d-49ea-89e4-0649f5eb20d4';
46
+
47
+ private taskTypeId: string | null = null;
48
+
49
+ constructor(
50
+ private contextUser: UserInfo,
51
+ private pubSub?: PubSubEngine,
52
+ private sessionId?: string,
53
+ private userPayload?: UserPayload
54
+ ) {}
55
+
56
+ /**
57
+ * Initialize the orchestrator by finding/creating the AI Agent Task type
58
+ */
59
+ private async ensureTaskType(): Promise<string> {
60
+ if (this.taskTypeId) {
61
+ return this.taskTypeId;
62
+ }
63
+
64
+ const rv = new RunView();
65
+ const result = await rv.RunView({
66
+ EntityName: 'MJ: Task Types',
67
+ ExtraFilter: `Name='AI Agent Execution'`,
68
+ ResultType: 'entity_object'
69
+ }, this.contextUser);
70
+
71
+ if (result.Success && result.Results && result.Results.length > 0) {
72
+ this.taskTypeId = result.Results[0].ID;
73
+ return this.taskTypeId;
74
+ }
75
+
76
+ // Create the task type if it doesn't exist
77
+ const md = new Metadata();
78
+ const taskType = await md.GetEntityObject<TaskTypeEntity>('MJ: Task Types', this.contextUser);
79
+ taskType.Name = 'AI Agent Execution';
80
+ taskType.Description = 'Task executed by an AI agent as part of conversation workflow';
81
+
82
+ const saved = await taskType.Save();
83
+ if (!saved) {
84
+ throw new Error('Failed to create AI Agent Execution task type');
85
+ }
86
+
87
+ this.taskTypeId = taskType.ID;
88
+ return this.taskTypeId;
89
+ }
90
+
91
+ /**
92
+ * Create tasks from LLM task graph response
93
+ * @param taskGraph Task graph from Conversation Manager
94
+ * @param conversationDetailId ID of the conversation detail that triggered this
95
+ * @param environmentId Environment ID
96
+ * @returns Object with parentTaskId and map of tempId -> actual TaskEntity ID
97
+ */
98
+ async createTasksFromGraph(
99
+ taskGraph: TaskGraphResponse,
100
+ conversationDetailId: string,
101
+ environmentId: string
102
+ ): Promise<{ parentTaskId: string; taskIdMap: Map<string, string> }> {
103
+ const taskTypeId = await this.ensureTaskType();
104
+ const md = new Metadata();
105
+ const tempIdToRealId = new Map<string, string>();
106
+
107
+ // Create parent workflow task
108
+ const parentTask = await md.GetEntityObject<TaskEntity>('MJ: Tasks', this.contextUser);
109
+ parentTask.Name = taskGraph.workflowName;
110
+ parentTask.Description = taskGraph.reasoning || 'AI-orchestrated workflow';
111
+ parentTask.TypeID = taskTypeId;
112
+ parentTask.EnvironmentID = environmentId;
113
+ parentTask.ConversationDetailID = conversationDetailId; // Parent links to conversation
114
+ parentTask.Status = 'In Progress'; // Workflow is in progress
115
+ parentTask.PercentComplete = 0;
116
+
117
+ const parentSaved = await parentTask.Save();
118
+ if (!parentSaved) {
119
+ throw new Error('Failed to create parent workflow task');
120
+ }
121
+
122
+ LogStatus(`Created parent workflow task: ${parentTask.Name} (${parentTask.ID})`);
123
+
124
+ // Deduplicate tasks by tempId (LLM sometimes returns duplicates)
125
+ const seenTempIds = new Set<string>();
126
+ const uniqueTasks = taskGraph.tasks.filter(task => {
127
+ if (seenTempIds.has(task.tempId)) {
128
+ LogError(`Duplicate tempId detected and ignored: ${task.tempId} (${task.name})`);
129
+ return false;
130
+ }
131
+ seenTempIds.add(task.tempId);
132
+ return true;
133
+ });
134
+
135
+ LogStatus(`Creating ${uniqueTasks.length} unique child tasks (${taskGraph.tasks.length - uniqueTasks.length} duplicates filtered)`);
136
+
137
+ // Create all child tasks
138
+ for (const taskDef of uniqueTasks) {
139
+ const task = await md.GetEntityObject<TaskEntity>('MJ: Tasks', this.contextUser);
140
+
141
+ // Find agent by name
142
+ const agent = await this.findAgentByName(taskDef.agentName);
143
+ if (!agent) {
144
+ LogError(`Agent not found: ${taskDef.agentName}`);
145
+ continue;
146
+ }
147
+
148
+ task.Name = taskDef.name;
149
+ task.Description = taskDef.description;
150
+ task.TypeID = taskTypeId;
151
+ task.EnvironmentID = environmentId;
152
+ task.ParentID = parentTask.ID; // Link to parent task
153
+ task.ConversationDetailID = conversationDetailId; // Link to conversation so agent runs can be tracked
154
+ task.AgentID = agent.ID;
155
+ task.Status = 'Pending';
156
+ task.PercentComplete = 0;
157
+
158
+ // Store input payload if provided
159
+ if (taskDef.inputPayload) {
160
+ const metadata = {
161
+ inputPayload: taskDef.inputPayload,
162
+ tempId: taskDef.tempId
163
+ };
164
+ // Store in a well-known format at the end of description
165
+ task.Description = `${taskDef.description}\n\n__TASK_METADATA__\n${JSON.stringify(metadata)}`;
166
+ }
167
+
168
+ const saved = await task.Save();
169
+ if (saved) {
170
+ tempIdToRealId.set(taskDef.tempId, task.ID);
171
+ LogStatus(`Created child task: ${task.Name} (${task.ID}) under parent ${parentTask.ID}`);
172
+ }
173
+ }
174
+
175
+ // Create dependencies between child tasks
176
+ for (const taskDef of uniqueTasks) {
177
+ const taskId = tempIdToRealId.get(taskDef.tempId);
178
+ if (!taskId) continue;
179
+
180
+ for (const dependsOnTempId of taskDef.dependsOn) {
181
+ const dependsOnId = tempIdToRealId.get(dependsOnTempId);
182
+ if (!dependsOnId) {
183
+ LogError(`Dependency not found: ${dependsOnTempId}`);
184
+ continue;
185
+ }
186
+
187
+ const dependency = await md.GetEntityObject<TaskDependencyEntity>('MJ: Task Dependencies', this.contextUser);
188
+ dependency.TaskID = taskId;
189
+ dependency.DependsOnTaskID = dependsOnId;
190
+ dependency.DependencyType = 'Prerequisite';
191
+
192
+ await dependency.Save();
193
+ LogStatus(`Created dependency: Task ${taskId} depends on ${dependsOnId}`);
194
+ }
195
+ }
196
+
197
+ return {
198
+ parentTaskId: parentTask.ID,
199
+ taskIdMap: tempIdToRealId
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Publish task progress update via PubSub
205
+ */
206
+ private publishTaskProgress(taskName: string, message: string, percentComplete: number): void {
207
+ if (!this.pubSub || !this.sessionId || !this.userPayload) {
208
+ LogStatus(`⚠️ PubSub not available for progress updates (pubSub: ${!!this.pubSub}, sessionId: ${!!this.sessionId}, userPayload: ${!!this.userPayload})`);
209
+ return;
210
+ }
211
+
212
+ const payload = {
213
+ message: JSON.stringify({
214
+ resolver: 'TaskOrchestrator',
215
+ type: 'TaskProgress',
216
+ status: 'ok',
217
+ data: {
218
+ taskName,
219
+ message,
220
+ percentComplete,
221
+ timestamp: new Date()
222
+ }
223
+ }),
224
+ sessionId: this.userPayload.sessionId
225
+ };
226
+
227
+ LogStatus(`📡 Publishing task progress: ${taskName} - ${message} (${percentComplete}%) to session ${this.userPayload.sessionId}`);
228
+ this.pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, payload);
229
+
230
+ LogStatus(`[Task: ${taskName}] ${message} (${percentComplete}%)`);
231
+ }
232
+
233
+ /**
234
+ * Publish agent progress update (nested within task)
235
+ */
236
+ private publishAgentProgress(taskName: string, agentStep: string, agentMessage: string): void {
237
+ if (!this.pubSub || !this.sessionId || !this.userPayload) {
238
+ LogStatus(`⚠️ PubSub not available for agent progress (pubSub: ${!!this.pubSub}, sessionId: ${!!this.sessionId}, userPayload: ${!!this.userPayload})`);
239
+ return;
240
+ }
241
+
242
+ const payload = {
243
+ message: JSON.stringify({
244
+ resolver: 'TaskOrchestrator',
245
+ type: 'AgentProgress',
246
+ status: 'ok',
247
+ data: {
248
+ taskName,
249
+ agentStep,
250
+ agentMessage,
251
+ timestamp: new Date()
252
+ }
253
+ }),
254
+ sessionId: this.userPayload.sessionId
255
+ };
256
+
257
+ LogStatus(`📡 Publishing agent progress: ${taskName} → ${agentStep} to session ${this.userPayload.sessionId}`);
258
+ this.pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, payload);
259
+
260
+ LogStatus(`[Task: ${taskName}] → ${agentStep}: ${agentMessage}`);
261
+ }
262
+
263
+ /**
264
+ * Find agent by name
265
+ */
266
+ private async findAgentByName(agentName: string): Promise<AIAgentEntityExtended | null> {
267
+ const rv = new RunView();
268
+ const result = await rv.RunView<AIAgentEntityExtended>({
269
+ EntityName: 'AI Agents',
270
+ ExtraFilter: `Name='${agentName.replace(/'/g, "''")}'`,
271
+ ResultType: 'entity_object'
272
+ }, this.contextUser);
273
+
274
+ if (result.Success && result.Results && result.Results.length > 0) {
275
+ return result.Results[0];
276
+ }
277
+
278
+ return null;
279
+ }
280
+
281
+ /**
282
+ * Execute all pending tasks for a parent task, respecting dependencies
283
+ * @param parentTaskId Parent task ID
284
+ * @returns Array of execution results
285
+ */
286
+ async executeTasksForParent(parentTaskId: string): Promise<TaskExecutionResult[]> {
287
+ const results: TaskExecutionResult[] = [];
288
+ let hasMore = true;
289
+
290
+ // Get parent task for progress updates
291
+ const md = new Metadata();
292
+ const parentTask = await md.GetEntityObject<TaskEntity>('MJ: Tasks', this.contextUser);
293
+ await parentTask.Load(parentTaskId);
294
+
295
+ // Publish workflow start
296
+ this.publishTaskProgress(parentTask.Name, 'Starting workflow execution', 0);
297
+
298
+ while (hasMore) {
299
+ // Find tasks that are pending and have no incomplete dependencies
300
+ const eligibleTasks = await this.findEligibleTasks(parentTaskId);
301
+
302
+ if (eligibleTasks.length === 0) {
303
+ hasMore = false;
304
+ break;
305
+ }
306
+
307
+ // Execute eligible tasks (could be parallelized in the future)
308
+ for (const task of eligibleTasks) {
309
+ // Publish task start
310
+ this.publishTaskProgress(task.Name, 'Starting task', 0);
311
+
312
+ const result = await this.executeTask(task);
313
+ results.push(result);
314
+
315
+ // Publish task complete
316
+ if (result.success) {
317
+ this.publishTaskProgress(task.Name, 'Task completed successfully', 100);
318
+ } else {
319
+ this.publishTaskProgress(task.Name, `Task failed: ${result.error}`, 100);
320
+ }
321
+
322
+ // Update parent task progress after each child completes
323
+ await this.updateParentTaskProgress(parentTaskId);
324
+ }
325
+ }
326
+
327
+ // Mark parent task as complete
328
+ await this.completeParentTask(parentTaskId);
329
+
330
+ // Publish workflow complete
331
+ this.publishTaskProgress(parentTask.Name, 'Workflow completed', 100);
332
+
333
+ return results;
334
+ }
335
+
336
+ /**
337
+ * Find tasks that are ready to execute (pending with no incomplete dependencies)
338
+ */
339
+ private async findEligibleTasks(parentTaskId: string): Promise<TaskEntity[]> {
340
+ const rv = new RunView();
341
+
342
+ // Get all pending tasks for this parent
343
+ const tasksResult = await rv.RunView<TaskEntity>({
344
+ EntityName: 'MJ: Tasks',
345
+ ExtraFilter: `ParentID='${parentTaskId}' AND Status='Pending'`,
346
+ ResultType: 'entity_object'
347
+ }, this.contextUser);
348
+
349
+ if (!tasksResult.Success || !tasksResult.Results) {
350
+ return [];
351
+ }
352
+
353
+ const eligibleTasks: TaskEntity[] = [];
354
+
355
+ // Check each task for incomplete dependencies
356
+ for (const task of tasksResult.Results) {
357
+ const hasIncompleteDeps = await this.hasIncompleteDependencies(task.ID);
358
+ if (!hasIncompleteDeps) {
359
+ eligibleTasks.push(task);
360
+ }
361
+ }
362
+
363
+ return eligibleTasks;
364
+ }
365
+
366
+ /**
367
+ * Update parent task progress based on child task completion
368
+ */
369
+ private async updateParentTaskProgress(parentTaskId: string): Promise<void> {
370
+ const md = new Metadata();
371
+ const parentTask = await md.GetEntityObject<TaskEntity>('MJ: Tasks', this.contextUser);
372
+ const loaded = await parentTask.Load(parentTaskId);
373
+ if (!loaded) return;
374
+
375
+ const rv = new RunView();
376
+
377
+ // Get all child tasks
378
+ const childrenResult = await rv.RunView<TaskEntity>({
379
+ EntityName: 'MJ: Tasks',
380
+ ExtraFilter: `ParentID='${parentTaskId}'`,
381
+ ResultType: 'entity_object'
382
+ }, this.contextUser);
383
+
384
+ if (!childrenResult.Success || !childrenResult.Results || childrenResult.Results.length === 0) {
385
+ return;
386
+ }
387
+
388
+ const children = childrenResult.Results;
389
+ const completedCount = children.filter(t => t.Status === 'Complete').length;
390
+ const totalCount = children.length;
391
+
392
+ // Update percent complete
393
+ parentTask.PercentComplete = Math.round((completedCount / totalCount) * 100);
394
+ await parentTask.Save();
395
+
396
+ LogStatus(`Parent task ${parentTask.Name} is ${parentTask.PercentComplete}% complete (${completedCount}/${totalCount} tasks)`);
397
+ }
398
+
399
+ /**
400
+ * Mark parent task as complete when all children are done
401
+ */
402
+ private async completeParentTask(parentTaskId: string): Promise<void> {
403
+ const md = new Metadata();
404
+ const parentTask = await md.GetEntityObject<TaskEntity>('MJ: Tasks', this.contextUser);
405
+ const loaded = await parentTask.Load(parentTaskId);
406
+ if (!loaded) return;
407
+
408
+ parentTask.Status = 'Complete';
409
+ parentTask.PercentComplete = 100;
410
+ parentTask.CompletedAt = new Date();
411
+ await parentTask.Save();
412
+
413
+ LogStatus(`Parent workflow task completed: ${parentTask.Name}`);
414
+ }
415
+
416
+ /**
417
+ * Check if a task has incomplete dependencies
418
+ */
419
+ private async hasIncompleteDependencies(taskId: string): Promise<boolean> {
420
+ const rv = new RunView();
421
+
422
+ // Get dependencies
423
+ const depsResult = await rv.RunView<TaskDependencyEntity>({
424
+ EntityName: 'MJ: Task Dependencies',
425
+ ExtraFilter: `TaskID='${taskId}'`,
426
+ ResultType: 'entity_object'
427
+ }, this.contextUser);
428
+
429
+ if (!depsResult.Success || !depsResult.Results || depsResult.Results.length === 0) {
430
+ return false; // No dependencies
431
+ }
432
+
433
+ // Check if any dependency is not complete
434
+ for (const dep of depsResult.Results) {
435
+ const dependsOnTask = await this.loadTask(dep.DependsOnTaskID);
436
+ if (dependsOnTask && dependsOnTask.Status !== 'Complete') {
437
+ return true; // Has incomplete dependency
438
+ }
439
+ }
440
+
441
+ return false;
442
+ }
443
+
444
+ /**
445
+ * Load a task by ID
446
+ */
447
+ private async loadTask(taskId: string): Promise<TaskEntity | null> {
448
+ const md = new Metadata();
449
+ const task = await md.GetEntityObject<TaskEntity>('MJ: Tasks', this.contextUser);
450
+ const loaded = await task.Load(taskId);
451
+ return loaded ? task : null;
452
+ }
453
+
454
+ /**
455
+ * Execute a single task
456
+ */
457
+ private async executeTask(task: TaskEntity): Promise<TaskExecutionResult> {
458
+ try {
459
+ LogStatus(`Executing task: ${task.Name} (${task.ID})`);
460
+
461
+ // Update status to In Progress
462
+ task.Status = 'In Progress';
463
+ task.StartedAt = new Date();
464
+ await task.Save();
465
+
466
+ // Load the agent entity
467
+ const md = new Metadata();
468
+ const agentEntity = await md.GetEntityObject<AIAgentEntityExtended>('AI Agents', this.contextUser);
469
+ const loaded = await agentEntity.Load(task.AgentID!);
470
+ if (!loaded) {
471
+ throw new Error(`Agent with ID ${task.AgentID} not found`);
472
+ }
473
+
474
+ // Build conversation messages with task input and dependent outputs as markdown
475
+ const messages = await this.buildConversationMessages(task);
476
+
477
+ // Create progress callback to publish agent progress nested under task
478
+ const onProgress = (progress: any) => {
479
+ this.publishAgentProgress(
480
+ task.Name,
481
+ progress.step || 'processing',
482
+ progress.message || ''
483
+ );
484
+ };
485
+
486
+ // Run the agent - use only conversationMessages, no payload parameter
487
+ // Payload should only be used when passing an agent its own prior output for modification
488
+ const agentRunner = new AgentRunner();
489
+ const agentResult = await agentRunner.RunAgent({
490
+ agent: agentEntity,
491
+ conversationMessages: messages,
492
+ contextUser: this.contextUser,
493
+ conversationDetailId: task.ConversationDetailID || undefined,
494
+ onProgress: onProgress
495
+ });
496
+
497
+ if (agentResult.success) {
498
+ // Extract output - check both message and payload
499
+ const output = this.extractAgentOutput(agentResult);
500
+
501
+ // Store output in task metadata
502
+ const outputMetadata = {
503
+ outputType: output.type,
504
+ output: output.content,
505
+ agentRunId: agentResult.agentRun?.ID
506
+ };
507
+
508
+ // Update task with success
509
+ task.Status = 'Complete';
510
+ task.CompletedAt = new Date();
511
+ task.PercentComplete = 100;
512
+ // Store output in description (would be better as a separate column)
513
+ task.Description = `${task.Description}\n\n__TASK_OUTPUT__\n${JSON.stringify(outputMetadata)}`;
514
+ await task.Save();
515
+
516
+ LogStatus(`Task completed: ${task.Name} (output type: ${output.type})`);
517
+
518
+ // Always create artifact for task output (both message and payload results)
519
+ let conversationDetailId = task.ConversationDetailID;
520
+ if (!conversationDetailId && task.ParentID) {
521
+ const parentTask = await this.loadTask(task.ParentID);
522
+ conversationDetailId = parentTask?.ConversationDetailID || null;
523
+ }
524
+
525
+ if (conversationDetailId && output.content) {
526
+ await this.createArtifactFromOutput(
527
+ output,
528
+ conversationDetailId,
529
+ agentEntity,
530
+ task.Name
531
+ );
532
+ } else if (!conversationDetailId) {
533
+ LogError(`Cannot create artifact: No conversation detail ID found for task ${task.ID}`);
534
+ }
535
+
536
+ return {
537
+ taskId: task.ID,
538
+ success: true,
539
+ output: output.content
540
+ };
541
+ } else {
542
+ // Update task with failure
543
+ task.Status = 'Failed';
544
+ task.CompletedAt = new Date();
545
+ await task.Save();
546
+
547
+ const errorMsg = agentResult.agentRun?.ErrorMessage || 'Agent execution failed';
548
+ LogError(`Task failed: ${task.Name} - ${errorMsg}`);
549
+
550
+ return {
551
+ taskId: task.ID,
552
+ success: false,
553
+ error: errorMsg
554
+ };
555
+ }
556
+ } catch (error) {
557
+ LogError(error);
558
+
559
+ // Update task with failure
560
+ task.Status = 'Failed';
561
+ task.CompletedAt = new Date();
562
+ await task.Save();
563
+
564
+ return {
565
+ taskId: task.ID,
566
+ success: false,
567
+ error: error instanceof Error ? error.message : 'Unknown error'
568
+ };
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Extract input payload from task metadata
574
+ */
575
+ private extractInputPayload(task: TaskEntity): any | null {
576
+ if (!task.Description) return null;
577
+
578
+ const metadataMatch = task.Description.match(/__TASK_METADATA__\n(.+?)(?:\n\n|$)/s);
579
+ if (!metadataMatch) return null;
580
+
581
+ try {
582
+ const metadata = JSON.parse(metadataMatch[1]);
583
+ return metadata.inputPayload || null;
584
+ } catch {
585
+ return null;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Get outputs from tasks that this task depends on
591
+ */
592
+ private async getDependentTaskOutputs(taskId: string): Promise<Map<string, any>> {
593
+ const outputs = new Map<string, any>();
594
+ const rv = new RunView();
595
+
596
+ // Get dependencies
597
+ const depsResult = await rv.RunView<TaskDependencyEntity>({
598
+ EntityName: 'MJ: Task Dependencies',
599
+ ExtraFilter: `TaskID='${taskId}'`,
600
+ ResultType: 'entity_object'
601
+ }, this.contextUser);
602
+
603
+ if (!depsResult.Success || !depsResult.Results) {
604
+ return outputs;
605
+ }
606
+
607
+ // Get output from each dependency
608
+ for (const dep of depsResult.Results) {
609
+ const dependsOnTask = await this.loadTask(dep.DependsOnTaskID);
610
+ if (!dependsOnTask || !dependsOnTask.Description) continue;
611
+
612
+ const outputMatch = dependsOnTask.Description.match(/__TASK_OUTPUT__\n(.+?)$/s);
613
+ if (outputMatch) {
614
+ try {
615
+ const outputMetadata = JSON.parse(outputMatch[1]);
616
+ outputs.set(dep.DependsOnTaskID, outputMetadata.output);
617
+ } catch (e) {
618
+ LogError(`Failed to parse output for task ${dep.DependsOnTaskID}: ${e}`);
619
+ }
620
+ }
621
+ }
622
+
623
+ return outputs;
624
+ }
625
+
626
+ /**
627
+ * Build conversation messages with task input and dependent outputs formatted as markdown
628
+ */
629
+ private async buildConversationMessages(task: TaskEntity): Promise<any[]> {
630
+ const messages: any[] = [];
631
+
632
+ // Start with task description/name as base content
633
+ let userContent = task.Description || task.Name;
634
+
635
+ // Extract input payload from task metadata if it exists
636
+ const inputPayload = this.extractInputPayload(task);
637
+
638
+ // Get dependent task outputs
639
+ const dependentOutputs = await this.getDependentTaskOutputs(task.ID);
640
+
641
+ // If there are dependent outputs, format them as markdown blocks
642
+ if (dependentOutputs.size > 0) {
643
+ userContent += '\n\n## Results from Dependent Tasks:\n\n';
644
+ for (const [taskId, outputData] of dependentOutputs.entries()) {
645
+ const depTask = await this.loadTask(taskId);
646
+ const taskName = depTask?.Name || taskId;
647
+ userContent += `### ${taskName}\n\`\`\`json\n${JSON.stringify(outputData, null, 2)}\n\`\`\`\n\n`;
648
+ }
649
+ }
650
+
651
+ // If input payload exists, add it as a separate section
652
+ if (inputPayload) {
653
+ userContent += '\n\n## Task Input:\n\`\`\`json\n' + JSON.stringify(inputPayload, null, 2) + '\n\`\`\`';
654
+ }
655
+
656
+ messages.push({
657
+ role: 'user' as ChatMessageRole,
658
+ content: userContent
659
+ });
660
+
661
+ return messages;
662
+ }
663
+
664
+ /**
665
+ * Extract agent output - check both message and payload
666
+ */
667
+ private extractAgentOutput(agentResult: any): { type: 'message' | 'payload', content: any } {
668
+ // Check if agent returned a message (text response)
669
+ if (agentResult.agentRun?.Message) {
670
+ return { type: 'message', content: agentResult.agentRun.Message };
671
+ }
672
+
673
+ // Check if agent returned a payload (structured data)
674
+ if (agentResult.payload && Object.keys(agentResult.payload).length > 0) {
675
+ return { type: 'payload', content: agentResult.payload };
676
+ }
677
+
678
+ // No output
679
+ return { type: 'message', content: '' };
680
+ }
681
+
682
+ /**
683
+ * Create artifact from task output (handles both message and payload types)
684
+ */
685
+ private async createArtifactFromOutput(
686
+ output: { type: 'message' | 'payload', content: any },
687
+ conversationDetailId: string,
688
+ agent: AIAgentEntityExtended,
689
+ taskName: string
690
+ ): Promise<void> {
691
+ try {
692
+ const md = new Metadata();
693
+
694
+ // Create Artifact header
695
+ const artifact = await md.GetEntityObject<ArtifactEntity>('MJ: Artifacts', this.contextUser);
696
+ artifact.Name = `${agent.Name} - ${taskName} - ${new Date().toLocaleString()}`;
697
+ artifact.Description = `Artifact generated by ${agent.Name} for task: ${taskName} (${output.type})`;
698
+
699
+ // Use agent's DefaultArtifactTypeID if available, otherwise fall back to JSON
700
+ const defaultArtifactTypeId = (agent as any).DefaultArtifactTypeID;
701
+ artifact.TypeID = defaultArtifactTypeId || this.JSON_ARTIFACT_TYPE_ID;
702
+
703
+ artifact.UserID = this.contextUser.ID;
704
+ artifact.EnvironmentID = (this.contextUser as any).EnvironmentID || 'F51358F3-9447-4176-B313-BF8025FD8D09';
705
+
706
+ const artifactSaved = await artifact.Save();
707
+ if (!artifactSaved) {
708
+ LogError('Failed to save artifact');
709
+ return;
710
+ }
711
+
712
+ LogStatus(`Created artifact: ${artifact.Name} (${artifact.ID})`);
713
+
714
+ // Create Artifact Version with content
715
+ const version = await md.GetEntityObject<ArtifactVersionEntity>('MJ: Artifact Versions', this.contextUser);
716
+ version.ArtifactID = artifact.ID;
717
+ version.VersionNumber = 1;
718
+
719
+ // Store content based on output type
720
+ if (output.type === 'message') {
721
+ version.Content = output.content;
722
+ } else {
723
+ version.Content = JSON.stringify(output.content, null, 2);
724
+ }
725
+
726
+ version.UserID = this.contextUser.ID;
727
+
728
+ const versionSaved = await version.Save();
729
+ if (!versionSaved) {
730
+ LogError('Failed to save artifact version');
731
+ return;
732
+ }
733
+
734
+ LogStatus(`Created artifact version: ${version.ID}`);
735
+
736
+ // Create M2M relationship linking artifact to conversation detail
737
+ const junction = await md.GetEntityObject<ConversationDetailArtifactEntity>(
738
+ 'MJ: Conversation Detail Artifacts',
739
+ this.contextUser
740
+ );
741
+ junction.ConversationDetailID = conversationDetailId;
742
+ junction.ArtifactVersionID = version.ID;
743
+ junction.Direction = 'Output'; // Artifact produced as output from task
744
+
745
+ const junctionSaved = await junction.Save();
746
+ if (!junctionSaved) {
747
+ LogError('Failed to create artifact-conversation association');
748
+ return;
749
+ }
750
+
751
+ LogStatus(`Linked artifact ${artifact.ID} to conversation detail ${conversationDetailId}`);
752
+ } catch (error) {
753
+ LogError(`Error creating artifact from output: ${error}`);
754
+ }
755
+ }
756
+ }