@memberjunction/server 6.1.0-edge.0 → 6.1.0-edge.2

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 (175) hide show
  1. package/dist/auth/exampleNewUserSubClass.js +1 -1
  2. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  3. package/dist/auth/index.d.ts.map +1 -1
  4. package/dist/auth/index.js +9 -5
  5. package/dist/auth/index.js.map +1 -1
  6. package/dist/auth/magicLink/MagicLinkService.js +1 -1
  7. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  8. package/dist/auth/newUsers.js +1 -1
  9. package/dist/auth/newUsers.js.map +1 -1
  10. package/dist/context.js +2 -2
  11. package/dist/context.js.map +1 -1
  12. package/dist/generated/generated.d.ts +208 -646
  13. package/dist/generated/generated.d.ts.map +1 -1
  14. package/dist/generated/generated.js +1123 -3773
  15. package/dist/generated/generated.js.map +1 -1
  16. package/dist/generic/ResolverBase.d.ts +19 -3
  17. package/dist/generic/ResolverBase.d.ts.map +1 -1
  18. package/dist/generic/ResolverBase.js +51 -17
  19. package/dist/generic/ResolverBase.js.map +1 -1
  20. package/dist/index.d.ts +6 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +100 -42
  23. package/dist/index.js.map +1 -1
  24. package/dist/logging/secretRedactor.d.ts +9 -0
  25. package/dist/logging/secretRedactor.d.ts.map +1 -1
  26. package/dist/logging/secretRedactor.js +39 -0
  27. package/dist/logging/secretRedactor.js.map +1 -1
  28. package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
  29. package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
  30. package/dist/middleware/BaseServerMiddleware.js +5 -0
  31. package/dist/middleware/BaseServerMiddleware.js.map +1 -1
  32. package/dist/realtimeWidget/WidgetSessionService.js +1 -1
  33. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  34. package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
  35. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  36. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  37. package/dist/resolvers/ActionResolver.js +12 -0
  38. package/dist/resolvers/ActionResolver.js.map +1 -1
  39. package/dist/resolvers/ComponentRegistryResolver.js +1 -1
  40. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  41. package/dist/resolvers/EntityPermissionResolver.js +1 -1
  42. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  43. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +9 -0
  44. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  45. package/dist/resolvers/IntegrationDiscoveryResolver.js +3 -1
  46. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  47. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  48. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  49. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  50. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  51. package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
  52. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  53. package/dist/resolvers/SearchEntitiesResolver.js +1 -1
  54. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
  55. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  56. package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
  57. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  58. package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
  59. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  60. package/dist/resolvers/TaskGraphFrameResolver.d.ts +85 -0
  61. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -0
  62. package/dist/resolvers/TaskGraphFrameResolver.js +163 -0
  63. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -0
  64. package/dist/resolvers/UserFavoriteResolver.js +1 -1
  65. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  66. package/dist/rest/OAuthCallbackHandler.js +1 -1
  67. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  68. package/dist/rest/RESTEndpointHandler.d.ts +0 -4
  69. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  70. package/dist/rest/RESTEndpointHandler.js +1 -15
  71. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  72. package/dist/rest/SignatureWebhookHandler.js +1 -1
  73. package/dist/rest/SignatureWebhookHandler.js.map +1 -1
  74. package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
  75. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
  76. package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
  77. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
  78. package/dist/services/ScheduledJobsService.js +1 -1
  79. package/dist/services/ScheduledJobsService.js.map +1 -1
  80. package/dist/services/StartTaskGraphDispatcher.d.ts +33 -0
  81. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -0
  82. package/dist/services/StartTaskGraphDispatcher.js +46 -0
  83. package/dist/services/StartTaskGraphDispatcher.js.map +1 -0
  84. package/dist/services/TaskGraphActionRunner.d.ts +34 -0
  85. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
  86. package/dist/services/TaskGraphActionRunner.js +95 -0
  87. package/dist/services/TaskGraphActionRunner.js.map +1 -0
  88. package/dist/services/TaskGraphAgentRunner.d.ts +28 -0
  89. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -0
  90. package/dist/services/TaskGraphAgentRunner.js +104 -0
  91. package/dist/services/TaskGraphAgentRunner.js.map +1 -0
  92. package/dist/services/TaskGraphContinuationDeliverer.d.ts +70 -0
  93. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -0
  94. package/dist/services/TaskGraphContinuationDeliverer.js +162 -0
  95. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -0
  96. package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
  97. package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
  98. package/dist/services/TaskGraphPromptRunner.js +134 -0
  99. package/dist/services/TaskGraphPromptRunner.js.map +1 -0
  100. package/dist/services/TaskGraphProviderFactory.d.ts +39 -0
  101. package/dist/services/TaskGraphProviderFactory.d.ts.map +1 -0
  102. package/dist/services/TaskGraphProviderFactory.js +28 -0
  103. package/dist/services/TaskGraphProviderFactory.js.map +1 -0
  104. package/dist/telephony/RingCentralTelephonyService.js +1 -1
  105. package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
  106. package/dist/telephony/TwilioTelephonyRouter.js +1 -1
  107. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
  108. package/dist/telephony/VonageTelephonyRouter.js +1 -1
  109. package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
  110. package/package.json +92 -89
  111. package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
  112. package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
  113. package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
  114. package/src/__tests__/SessionManager.test.ts +1 -1
  115. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +249 -0
  116. package/src/__tests__/TaskGraphFrameResolver.test.ts +136 -0
  117. package/src/__tests__/WidgetSessionService.test.ts +1 -1
  118. package/src/__tests__/databaseAbstraction.test.ts +1 -1
  119. package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
  120. package/src/__tests__/resolverBase.rls.test.ts +3 -0
  121. package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
  122. package/src/__tests__/widgetGuestElevation.test.ts +1 -1
  123. package/src/auth/exampleNewUserSubClass.ts +1 -1
  124. package/src/auth/index.ts +10 -6
  125. package/src/auth/magicLink/MagicLinkService.ts +1 -1
  126. package/src/auth/newUsers.ts +1 -1
  127. package/src/context.ts +2 -2
  128. package/src/generated/generated.ts +955 -2735
  129. package/src/generic/ResolverBase.ts +52 -17
  130. package/src/index.ts +108 -43
  131. package/src/logging/secretRedactor.ts +43 -0
  132. package/src/middleware/BaseServerMiddleware.ts +5 -0
  133. package/src/realtimeWidget/WidgetSessionService.ts +1 -1
  134. package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
  135. package/src/resolvers/ActionResolver.ts +13 -0
  136. package/src/resolvers/ComponentRegistryResolver.ts +1 -1
  137. package/src/resolvers/EntityPermissionResolver.ts +1 -1
  138. package/src/resolvers/IntegrationDiscoveryResolver.ts +12 -2
  139. package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
  140. package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
  141. package/src/resolvers/SearchEntitiesResolver.ts +1 -1
  142. package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
  143. package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
  144. package/src/resolvers/TaskGraphFrameResolver.ts +160 -0
  145. package/src/resolvers/UserFavoriteResolver.ts +1 -1
  146. package/src/rest/OAuthCallbackHandler.ts +1 -1
  147. package/src/rest/RESTEndpointHandler.ts +1 -15
  148. package/src/rest/SignatureWebhookHandler.ts +1 -1
  149. package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
  150. package/src/services/ScheduledJobsService.ts +1 -1
  151. package/src/services/StartTaskGraphDispatcher.ts +78 -0
  152. package/src/services/TaskGraphActionRunner.ts +99 -0
  153. package/src/services/TaskGraphAgentRunner.ts +115 -0
  154. package/src/services/TaskGraphContinuationDeliverer.ts +181 -0
  155. package/src/services/TaskGraphPromptRunner.ts +144 -0
  156. package/src/services/TaskGraphProviderFactory.ts +53 -0
  157. package/src/telephony/RingCentralTelephonyService.ts +1 -1
  158. package/src/telephony/TwilioTelephonyRouter.ts +1 -1
  159. package/src/telephony/VonageTelephonyRouter.ts +1 -1
  160. package/dist/resolvers/ReportResolver.d.ts +0 -24
  161. package/dist/resolvers/ReportResolver.d.ts.map +0 -1
  162. package/dist/resolvers/ReportResolver.js +0 -198
  163. package/dist/resolvers/ReportResolver.js.map +0 -1
  164. package/dist/resolvers/TaskResolver.d.ts +0 -31
  165. package/dist/resolvers/TaskResolver.d.ts.map +0 -1
  166. package/dist/resolvers/TaskResolver.js +0 -157
  167. package/dist/resolvers/TaskResolver.js.map +0 -1
  168. package/dist/services/TaskOrchestrator.d.ts +0 -130
  169. package/dist/services/TaskOrchestrator.d.ts.map +0 -1
  170. package/dist/services/TaskOrchestrator.js +0 -692
  171. package/dist/services/TaskOrchestrator.js.map +0 -1
  172. package/src/resolvers/ReportResolver.ts +0 -165
  173. package/src/resolvers/TaskResolver.ts +0 -148
  174. package/src/resolvers/__tests__/ReportResolver.test.ts +0 -279
  175. package/src/services/TaskOrchestrator.ts +0 -873
@@ -1,873 +0,0 @@
1
- import { DatabaseProviderBase, IMetadataProvider, Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
2
- import { MJTaskEntity, MJTaskDependencyEntity, MJTaskTypeEntity, MJConversationDetailEntity, MJArtifactEntity, MJArtifactVersionEntity, MJConversationDetailArtifactEntity, MJUserNotificationEntity } 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 { publishStatusUpdate } from '../generic/PushStatusResolver.js';
8
- import { MJAIAgentEntityExtended } from '@memberjunction/ai-core-plus';
9
-
10
- /**
11
- * Task definition from LLM response
12
- */
13
- export interface TaskDefinition {
14
- tempId: string; // LLM-generated ID for reference
15
- name: string;
16
- description: string;
17
- agentName: string;
18
- dependsOn: string[]; // Array of tempIds this task depends on
19
- inputPayload?: any;
20
- }
21
-
22
- /**
23
- * Task graph response from Conversation Manager
24
- */
25
- export interface TaskGraphResponse {
26
- workflowName: string; // Name for the parent/workflow task
27
- tasks: TaskDefinition[];
28
- reasoning?: string;
29
- }
30
-
31
- /**
32
- * Task execution result
33
- */
34
- export interface TaskExecutionResult {
35
- taskId: string;
36
- success: boolean;
37
- output?: any;
38
- error?: string;
39
- }
40
-
41
- /**
42
- * TaskOrchestrator handles multi-step task execution with dependencies
43
- */
44
- export class TaskOrchestrator {
45
- // Default artifact type ID for JSON (when agent doesn't specify DefaultArtifactTypeID)
46
- private readonly JSON_ARTIFACT_TYPE_ID = 'ae674c7e-ea0d-49ea-89e4-0649f5eb20d4';
47
-
48
- private taskTypeId: string | null = null;
49
-
50
- constructor(
51
- private contextUser: UserInfo,
52
- private pubSub?: PubSubEngine,
53
- private sessionId?: string,
54
- private userPayload?: UserPayload,
55
- private createNotifications: boolean = false,
56
- private conversationDetailId?: string,
57
- private provider?: IMetadataProvider
58
- ) {}
59
-
60
- private getMetadata(): IMetadataProvider {
61
- return this.provider ?? (new Metadata() as unknown as IMetadataProvider);
62
- }
63
-
64
- /**
65
- * Initialize the orchestrator by finding/creating the AI Agent Task type
66
- */
67
- private async ensureTaskType(): Promise<string> {
68
- if (this.taskTypeId) {
69
- return this.taskTypeId;
70
- }
71
-
72
- const rv = new RunView();
73
- const result = await rv.RunView({
74
- EntityName: 'MJ: Task Types',
75
- ExtraFilter: `Name='AI Agent Execution'`,
76
- ResultType: 'entity_object'
77
- }, this.contextUser);
78
-
79
- if (result.Success && result.Results && result.Results.length > 0) {
80
- this.taskTypeId = result.Results[0].ID;
81
- return this.taskTypeId;
82
- }
83
-
84
- // Create the task type if it doesn't exist
85
- const md = this.getMetadata();
86
- const taskType = await md.GetEntityObject<MJTaskTypeEntity>('MJ: Task Types', this.contextUser);
87
- taskType.Name = 'AI Agent Execution';
88
- taskType.Description = 'Task executed by an AI agent as part of conversation workflow';
89
-
90
- const saved = await taskType.Save();
91
- if (!saved) {
92
- throw new Error('Failed to create AI Agent Execution task type');
93
- }
94
-
95
- this.taskTypeId = taskType.ID;
96
- return this.taskTypeId;
97
- }
98
-
99
- /**
100
- * Create tasks from LLM task graph response
101
- * @param taskGraph Task graph from Conversation Manager
102
- * @param conversationDetailId ID of the conversation detail that triggered this
103
- * @param environmentId Environment ID
104
- * @returns Object with parentTaskId and map of tempId -> actual MJTaskEntity ID
105
- */
106
- async createTasksFromGraph(
107
- taskGraph: TaskGraphResponse,
108
- conversationDetailId: string,
109
- environmentId: string
110
- ): Promise<{ parentTaskId: string; taskIdMap: Map<string, string> }> {
111
- const taskTypeId = await this.ensureTaskType();
112
- const md = this.getMetadata();
113
- const tempIdToRealId = new Map<string, string>();
114
-
115
- // Build the parent task, deduplicate the incoming task defs, and resolve agents
116
- // BEFORE opening the transaction so all preparatory work (cache lookups, agent
117
- // resolution RunViews) happens outside the critical section.
118
- const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
119
- parentTask.Name = taskGraph.workflowName;
120
- parentTask.Description = taskGraph.reasoning || 'AI-orchestrated workflow';
121
- parentTask.TypeID = taskTypeId;
122
- parentTask.EnvironmentID = environmentId;
123
- parentTask.ConversationDetailID = conversationDetailId;
124
- parentTask.Status = 'In Progress';
125
- parentTask.PercentComplete = 0;
126
-
127
- const seenTempIds = new Set<string>();
128
- const uniqueTasks = taskGraph.tasks.filter(task => {
129
- if (seenTempIds.has(task.tempId)) {
130
- LogError(`Duplicate tempId detected and ignored: ${task.tempId} (${task.name})`);
131
- return false;
132
- }
133
- seenTempIds.add(task.tempId);
134
- return true;
135
- });
136
-
137
- LogStatus(`Preparing parent + ${uniqueTasks.length} unique child tasks (${taskGraph.tasks.length - uniqueTasks.length} duplicates filtered)`);
138
-
139
- const resolvedTasks: Array<{ def: TaskDefinition; agentId: string }> = [];
140
- for (const taskDef of uniqueTasks) {
141
- const agent = await this.findAgentByName(taskDef.agentName);
142
- if (!agent) {
143
- LogError(`Agent not found: ${taskDef.agentName}`);
144
- continue;
145
- }
146
- resolvedTasks.push({ def: taskDef, agentId: agent.ID });
147
- }
148
-
149
- // Persist parent + children + dependency graph in one transaction
150
- const provider = (this.provider ?? Metadata.Provider) as DatabaseProviderBase;
151
- await provider.BeginTransaction();
152
- try {
153
- if (!await parentTask.Save()) {
154
- throw new Error(`Failed to create parent workflow task: ${parentTask.LatestResult?.CompleteMessage ?? 'unknown error'}`);
155
- }
156
- LogStatus(`Created parent workflow task: ${parentTask.Name} (${parentTask.ID})`);
157
-
158
- for (const { def, agentId } of resolvedTasks) {
159
- const task = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
160
- task.Name = def.name;
161
- task.Description = def.description;
162
- task.TypeID = taskTypeId;
163
- task.EnvironmentID = environmentId;
164
- task.ParentID = parentTask.ID;
165
- task.ConversationDetailID = conversationDetailId;
166
- task.AgentID = agentId;
167
- task.Status = 'Pending';
168
- task.PercentComplete = 0;
169
-
170
- if (def.inputPayload) {
171
- const metadata = {
172
- inputPayload: def.inputPayload,
173
- tempId: def.tempId
174
- };
175
- task.Description = `${def.description}\n\n__TASK_METADATA__\n${JSON.stringify(metadata)}`;
176
- }
177
-
178
- if (!await task.Save()) {
179
- throw new Error(`Failed to create child task '${def.name}': ${task.LatestResult?.CompleteMessage ?? 'unknown error'}`);
180
- }
181
- tempIdToRealId.set(def.tempId, task.ID);
182
- LogStatus(`Created child task: ${task.Name} (${task.ID}) under parent ${parentTask.ID}`);
183
- }
184
-
185
- for (const { def } of resolvedTasks) {
186
- const taskId = tempIdToRealId.get(def.tempId);
187
- if (!taskId) continue;
188
-
189
- for (const dependsOnTempId of def.dependsOn) {
190
- const dependsOnId = tempIdToRealId.get(dependsOnTempId);
191
- if (!dependsOnId) {
192
- LogError(`Dependency not found: ${dependsOnTempId}`);
193
- continue;
194
- }
195
-
196
- const dependency = await md.GetEntityObject<MJTaskDependencyEntity>('MJ: Task Dependencies', this.contextUser);
197
- dependency.TaskID = taskId;
198
- dependency.DependsOnTaskID = dependsOnId;
199
- dependency.DependencyType = 'Prerequisite';
200
-
201
- if (!await dependency.Save()) {
202
- throw new Error(`Failed to create task dependency (${taskId} -> ${dependsOnId}): ${dependency.LatestResult?.CompleteMessage ?? 'unknown error'}`);
203
- }
204
- LogStatus(`Created dependency: Task ${taskId} depends on ${dependsOnId}`);
205
- }
206
- }
207
-
208
- await provider.CommitTransaction();
209
- } catch (txErr) {
210
- await provider.RollbackTransaction();
211
- throw txErr;
212
- }
213
-
214
- return {
215
- parentTaskId: parentTask.ID,
216
- taskIdMap: tempIdToRealId
217
- };
218
- }
219
-
220
- /**
221
- * Publish task progress update via PubSub
222
- */
223
- private publishTaskProgress(taskName: string, message: string, percentComplete: number): void {
224
- if (!this.pubSub || !this.sessionId || !this.userPayload) {
225
- LogStatus(`⚠️ PubSub not available for progress updates (pubSub: ${!!this.pubSub}, sessionId: ${!!this.sessionId}, userPayload: ${!!this.userPayload})`);
226
- return;
227
- }
228
-
229
- LogStatus(`📡 Publishing task progress: ${taskName} - ${message} (${percentComplete}%) to session ${this.userPayload.sessionId}`);
230
- publishStatusUpdate(this.pubSub, {
231
- sessionId: this.userPayload.sessionId,
232
- ownerUserId: this.userPayload.userRecord.ID,
233
- message: JSON.stringify({
234
- resolver: 'TaskOrchestrator',
235
- type: 'TaskProgress',
236
- status: 'ok',
237
- data: {
238
- taskName,
239
- message,
240
- percentComplete,
241
- timestamp: new Date(),
242
- conversationDetailId: this.conversationDetailId
243
- }
244
- })
245
- });
246
-
247
- LogStatus(`[Task: ${taskName}] ${message} (${percentComplete}%)`);
248
- }
249
-
250
- /**
251
- * Publish agent progress update (nested within task)
252
- */
253
- private publishAgentProgress(taskName: string, agentStep: string, agentMessage: string): void {
254
- if (!this.pubSub || !this.sessionId || !this.userPayload) {
255
- LogStatus(`⚠️ PubSub not available for agent progress (pubSub: ${!!this.pubSub}, sessionId: ${!!this.sessionId}, userPayload: ${!!this.userPayload})`);
256
- return;
257
- }
258
-
259
- LogStatus(`📡 Publishing agent progress: ${taskName} → ${agentStep} to session ${this.userPayload.sessionId}`);
260
- publishStatusUpdate(this.pubSub, {
261
- sessionId: this.userPayload.sessionId,
262
- ownerUserId: this.userPayload.userRecord.ID,
263
- message: JSON.stringify({
264
- resolver: 'TaskOrchestrator',
265
- type: 'AgentProgress',
266
- status: 'ok',
267
- data: {
268
- taskName,
269
- agentStep,
270
- agentMessage,
271
- timestamp: new Date(),
272
- conversationDetailId: this.conversationDetailId
273
- }
274
- })
275
- });
276
-
277
- LogStatus(`[Task: ${taskName}] → ${agentStep}: ${agentMessage}`);
278
- }
279
-
280
- /**
281
- * Find agent by name
282
- */
283
- private async findAgentByName(agentName: string): Promise<MJAIAgentEntityExtended | null> {
284
- const rv = new RunView();
285
- const result = await rv.RunView<MJAIAgentEntityExtended>({
286
- EntityName: 'MJ: AI Agents',
287
- ExtraFilter: `Name='${agentName.replace(/'/g, "''")}'`,
288
- ResultType: 'entity_object'
289
- }, this.contextUser);
290
-
291
- if (result.Success && result.Results && result.Results.length > 0) {
292
- return result.Results[0];
293
- }
294
-
295
- return null;
296
- }
297
-
298
- /**
299
- * Execute all pending tasks for a parent task, respecting dependencies
300
- * @param parentTaskId Parent task ID
301
- * @returns Array of execution results
302
- */
303
- async executeTasksForParent(parentTaskId: string): Promise<TaskExecutionResult[]> {
304
- const results: TaskExecutionResult[] = [];
305
- let hasMore = true;
306
-
307
- // Get parent task for progress updates
308
- const md = this.getMetadata();
309
- const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
310
- await parentTask.Load(parentTaskId);
311
-
312
- // Publish workflow start
313
- this.publishTaskProgress(parentTask.Name, 'Starting workflow execution', 0);
314
-
315
- while (hasMore) {
316
- // Find tasks that are pending and have no incomplete dependencies
317
- const eligibleTasks = await this.findEligibleTasks(parentTaskId);
318
-
319
- if (eligibleTasks.length === 0) {
320
- hasMore = false;
321
- break;
322
- }
323
-
324
- // Execute eligible tasks (could be parallelized in the future)
325
- for (const task of eligibleTasks) {
326
- // Publish task start
327
- this.publishTaskProgress(task.Name, 'Starting task', 0);
328
-
329
- const result = await this.executeTask(task);
330
- results.push(result);
331
-
332
- // Publish task complete
333
- if (result.success) {
334
- this.publishTaskProgress(task.Name, 'Task completed successfully', 100);
335
- } else {
336
- this.publishTaskProgress(task.Name, `Task failed: ${result.error}`, 100);
337
- }
338
-
339
- // Update parent task progress after each child completes
340
- await this.updateParentTaskProgress(parentTaskId);
341
- }
342
- }
343
-
344
- // Mark parent task as complete
345
- await this.completeParentTask(parentTaskId);
346
-
347
- // Publish workflow complete
348
- this.publishTaskProgress(parentTask.Name, 'Workflow completed', 100);
349
-
350
- return results;
351
- }
352
-
353
- /**
354
- * Find tasks that are ready to execute (pending with no incomplete dependencies)
355
- */
356
- private async findEligibleTasks(parentTaskId: string): Promise<MJTaskEntity[]> {
357
- const rv = new RunView();
358
-
359
- // Get all pending tasks for this parent
360
- const tasksResult = await rv.RunView<MJTaskEntity>({
361
- EntityName: 'MJ: Tasks',
362
- ExtraFilter: `ParentID='${parentTaskId}' AND Status='Pending'`,
363
- ResultType: 'entity_object'
364
- }, this.contextUser);
365
-
366
- if (!tasksResult.Success || !tasksResult.Results) {
367
- return [];
368
- }
369
-
370
- const eligibleTasks: MJTaskEntity[] = [];
371
-
372
- // Check each task for incomplete dependencies
373
- for (const task of tasksResult.Results) {
374
- const hasIncompleteDeps = await this.hasIncompleteDependencies(task.ID);
375
- if (!hasIncompleteDeps) {
376
- eligibleTasks.push(task);
377
- }
378
- }
379
-
380
- return eligibleTasks;
381
- }
382
-
383
- /**
384
- * Update parent task progress based on child task completion
385
- */
386
- private async updateParentTaskProgress(parentTaskId: string): Promise<void> {
387
- const md = this.getMetadata();
388
- const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
389
- const loaded = await parentTask.Load(parentTaskId);
390
- if (!loaded) return;
391
-
392
- const rv = new RunView();
393
-
394
- // Get all child tasks
395
- const childrenResult = await rv.RunView<MJTaskEntity>({
396
- EntityName: 'MJ: Tasks',
397
- ExtraFilter: `ParentID='${parentTaskId}'`,
398
- ResultType: 'entity_object'
399
- }, this.contextUser);
400
-
401
- if (!childrenResult.Success || !childrenResult.Results || childrenResult.Results.length === 0) {
402
- return;
403
- }
404
-
405
- const children = childrenResult.Results;
406
- const completedCount = children.filter(t => t.Status === 'Complete').length;
407
- const totalCount = children.length;
408
-
409
- // Update percent complete
410
- parentTask.PercentComplete = Math.round((completedCount / totalCount) * 100);
411
- await parentTask.Save();
412
-
413
- LogStatus(`Parent task ${parentTask.Name} is ${parentTask.PercentComplete}% complete (${completedCount}/${totalCount} tasks)`);
414
- }
415
-
416
- /**
417
- * Mark parent task as complete when all children are done
418
- */
419
- private async completeParentTask(parentTaskId: string): Promise<void> {
420
- const md = this.getMetadata();
421
- const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
422
- const loaded = await parentTask.Load(parentTaskId);
423
- if (!loaded) return;
424
-
425
- parentTask.Status = 'Complete';
426
- parentTask.PercentComplete = 100;
427
- parentTask.CompletedAt = new Date();
428
- const saved = await parentTask.Save();
429
-
430
- LogStatus(`Parent workflow task completed: ${parentTask.Name}`);
431
-
432
- // If notifications enabled, create user notification
433
- if (this.createNotifications && saved) {
434
- await this.createTaskGraphCompletionNotification(parentTask);
435
- }
436
- }
437
-
438
- /**
439
- * Check if a task has incomplete dependencies
440
- */
441
- private async hasIncompleteDependencies(taskId: string): Promise<boolean> {
442
- const rv = new RunView();
443
-
444
- // Get dependencies
445
- const depsResult = await rv.RunView<MJTaskDependencyEntity>({
446
- EntityName: 'MJ: Task Dependencies',
447
- ExtraFilter: `TaskID='${taskId}'`,
448
- ResultType: 'entity_object'
449
- }, this.contextUser);
450
-
451
- if (!depsResult.Success || !depsResult.Results || depsResult.Results.length === 0) {
452
- return false; // No dependencies
453
- }
454
-
455
- // Check if any dependency is not complete
456
- for (const dep of depsResult.Results) {
457
- const dependsOnTask = await this.loadTask(dep.DependsOnTaskID);
458
- if (dependsOnTask && dependsOnTask.Status !== 'Complete') {
459
- return true; // Has incomplete dependency
460
- }
461
- }
462
-
463
- return false;
464
- }
465
-
466
- /**
467
- * Load a task by ID
468
- */
469
- private async loadTask(taskId: string): Promise<MJTaskEntity | null> {
470
- const md = this.getMetadata();
471
- const task = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
472
- const loaded = await task.Load(taskId);
473
- return loaded ? task : null;
474
- }
475
-
476
- /**
477
- * Execute a single task
478
- */
479
- private async executeTask(task: MJTaskEntity): Promise<TaskExecutionResult> {
480
- try {
481
- LogStatus(`Executing task: ${task.Name} (${task.ID})`);
482
-
483
- // Update status to In Progress
484
- task.Status = 'In Progress';
485
- task.StartedAt = new Date();
486
- await task.Save();
487
-
488
- // Load the agent entity
489
- const md = this.getMetadata();
490
- const agentEntity = await md.GetEntityObject<MJAIAgentEntityExtended>('MJ: AI Agents', this.contextUser);
491
- const loaded = await agentEntity.Load(task.AgentID!);
492
- if (!loaded) {
493
- throw new Error(`Agent with ID ${task.AgentID} not found`);
494
- }
495
-
496
- // Build conversation messages with task input and dependent outputs as markdown
497
- const messages = await this.buildConversationMessages(task);
498
-
499
- // Create progress callback to publish agent progress nested under task
500
- const onProgress = (progress: any) => {
501
- this.publishAgentProgress(
502
- task.Name,
503
- progress.step || 'processing',
504
- progress.message || ''
505
- );
506
- };
507
-
508
- // Run the agent - use only conversationMessages, no payload parameter
509
- // Payload should only be used when passing an agent its own prior output for modification
510
- const agentRunner = new AgentRunner();
511
- const agentResult = await agentRunner.RunAgent({
512
- agent: agentEntity,
513
- conversationMessages: messages,
514
- contextUser: this.contextUser,
515
- conversationDetailId: task.ConversationDetailID || undefined,
516
- onProgress: onProgress
517
- });
518
-
519
- if (agentResult.success) {
520
- // Extract output - check both message and payload
521
- const output = this.extractAgentOutput(agentResult);
522
-
523
- // Store output in task metadata
524
- const outputMetadata = {
525
- outputType: output.type,
526
- output: output.content,
527
- agentRunId: agentResult.agentRun?.ID
528
- };
529
-
530
- // Update task with success
531
- task.Status = 'Complete';
532
- task.CompletedAt = new Date();
533
- task.PercentComplete = 100;
534
- // Store output in description (would be better as a separate column)
535
- task.Description = `${task.Description}\n\n__TASK_OUTPUT__\n${JSON.stringify(outputMetadata)}`;
536
- await task.Save();
537
-
538
- LogStatus(`Task completed: ${task.Name} (output type: ${output.type})`);
539
-
540
- // Always create artifact for task output (both message and payload results)
541
- let conversationDetailId = task.ConversationDetailID;
542
- if (!conversationDetailId && task.ParentID) {
543
- const parentTask = await this.loadTask(task.ParentID);
544
- conversationDetailId = parentTask?.ConversationDetailID || null;
545
- }
546
-
547
- if (conversationDetailId && output.content) {
548
- await this.createArtifactFromOutput(
549
- output,
550
- conversationDetailId,
551
- agentEntity,
552
- task.Name
553
- );
554
- } else if (!conversationDetailId) {
555
- LogError(`Cannot create artifact: No conversation detail ID found for task ${task.ID}`);
556
- }
557
-
558
- return {
559
- taskId: task.ID,
560
- success: true,
561
- output: output.content
562
- };
563
- } else {
564
- // Update task with failure
565
- task.Status = 'Failed';
566
- task.CompletedAt = new Date();
567
- await task.Save();
568
-
569
- const errorMsg = agentResult.agentRun?.ErrorMessage || 'Agent execution failed';
570
- LogError(`Task failed: ${task.Name} - ${errorMsg}`);
571
-
572
- return {
573
- taskId: task.ID,
574
- success: false,
575
- error: errorMsg
576
- };
577
- }
578
- } catch (error) {
579
- LogError(error);
580
-
581
- // Update task with failure
582
- task.Status = 'Failed';
583
- task.CompletedAt = new Date();
584
- await task.Save();
585
-
586
- return {
587
- taskId: task.ID,
588
- success: false,
589
- error: error instanceof Error ? error.message : 'Unknown error'
590
- };
591
- }
592
- }
593
-
594
- /**
595
- * Extract input payload from task metadata
596
- */
597
- private extractInputPayload(task: MJTaskEntity): any | null {
598
- if (!task.Description) return null;
599
-
600
- const metadataMatch = task.Description.match(/__TASK_METADATA__\n(.+?)(?:\n\n|$)/s);
601
- if (!metadataMatch) return null;
602
-
603
- try {
604
- const metadata = JSON.parse(metadataMatch[1]);
605
- return metadata.inputPayload || null;
606
- } catch {
607
- return null;
608
- }
609
- }
610
-
611
- /**
612
- * Get outputs from tasks that this task depends on
613
- */
614
- private async getDependentTaskOutputs(taskId: string): Promise<Map<string, any>> {
615
- const outputs = new Map<string, any>();
616
- const rv = new RunView();
617
-
618
- // Get dependencies
619
- const depsResult = await rv.RunView<MJTaskDependencyEntity>({
620
- EntityName: 'MJ: Task Dependencies',
621
- ExtraFilter: `TaskID='${taskId}'`,
622
- ResultType: 'entity_object'
623
- }, this.contextUser);
624
-
625
- if (!depsResult.Success || !depsResult.Results) {
626
- return outputs;
627
- }
628
-
629
- // Get output from each dependency
630
- for (const dep of depsResult.Results) {
631
- const dependsOnTask = await this.loadTask(dep.DependsOnTaskID);
632
- if (!dependsOnTask || !dependsOnTask.Description) continue;
633
-
634
- const outputMatch = dependsOnTask.Description.match(/__TASK_OUTPUT__\n(.+?)$/s);
635
- if (outputMatch) {
636
- try {
637
- const outputMetadata = JSON.parse(outputMatch[1]);
638
- outputs.set(dep.DependsOnTaskID, outputMetadata.output);
639
- } catch (e) {
640
- LogError(`Failed to parse output for task ${dep.DependsOnTaskID}: ${e}`);
641
- }
642
- }
643
- }
644
-
645
- return outputs;
646
- }
647
-
648
- /**
649
- * Build conversation messages with task input and dependent outputs formatted as markdown
650
- */
651
- private async buildConversationMessages(task: MJTaskEntity): Promise<any[]> {
652
- const messages: any[] = [];
653
-
654
- // Start with task description/name as base content
655
- let userContent = task.Description || task.Name;
656
-
657
- // Extract input payload from task metadata if it exists
658
- const inputPayload = this.extractInputPayload(task);
659
-
660
- // Get dependent task outputs
661
- const dependentOutputs = await this.getDependentTaskOutputs(task.ID);
662
-
663
- // If there are dependent outputs, format them as markdown blocks
664
- if (dependentOutputs.size > 0) {
665
- userContent += '\n\n## Results from Dependent Tasks:\n\n';
666
- for (const [taskId, outputData] of dependentOutputs.entries()) {
667
- const depTask = await this.loadTask(taskId);
668
- const taskName = depTask?.Name || taskId;
669
- userContent += `### ${taskName}\n\`\`\`json\n${JSON.stringify(outputData, null, 2)}\n\`\`\`\n\n`;
670
- }
671
- }
672
-
673
- // If input payload exists, add it as a separate section
674
- if (inputPayload) {
675
- userContent += '\n\n## Task Input:\n\`\`\`json\n' + JSON.stringify(inputPayload, null, 2) + '\n\`\`\`';
676
- }
677
-
678
- messages.push({
679
- role: 'user' as ChatMessageRole,
680
- content: userContent
681
- });
682
-
683
- return messages;
684
- }
685
-
686
- /**
687
- * Extract agent output - check both message and payload
688
- */
689
- private extractAgentOutput(agentResult: any): { type: 'message' | 'payload', content: any } {
690
- // Check if agent returned a message (text response)
691
- if (agentResult.agentRun?.Message) {
692
- return { type: 'message', content: agentResult.agentRun.Message };
693
- }
694
-
695
- // Check if agent returned a payload (structured data)
696
- if (agentResult.payload && Object.keys(agentResult.payload).length > 0) {
697
- return { type: 'payload', content: agentResult.payload };
698
- }
699
-
700
- // No output
701
- return { type: 'message', content: '' };
702
- }
703
-
704
- /**
705
- * Create artifact from task output (handles both message and payload types)
706
- */
707
- private async createArtifactFromOutput(
708
- output: { type: 'message' | 'payload', content: any },
709
- conversationDetailId: string,
710
- agent: MJAIAgentEntityExtended,
711
- taskName: string
712
- ): Promise<void> {
713
- const md = this.getMetadata();
714
- const provider = (this.provider ?? Metadata.Provider) as DatabaseProviderBase;
715
-
716
- await provider.BeginTransaction();
717
- try {
718
- // Create Artifact header
719
- const artifact = await md.GetEntityObject<MJArtifactEntity>('MJ: Artifacts', this.contextUser);
720
- artifact.Name = `${agent.Name} - ${taskName} - ${new Date().toLocaleString()}`;
721
- artifact.Description = `Artifact generated by ${agent.Name} for task: ${taskName} (${output.type})`;
722
-
723
- const defaultArtifactTypeId = (agent as any).DefaultArtifactTypeID;
724
- artifact.TypeID = defaultArtifactTypeId || this.JSON_ARTIFACT_TYPE_ID;
725
-
726
- artifact.UserID = this.contextUser.ID;
727
- artifact.EnvironmentID = (this.contextUser as any).EnvironmentID || 'F51358F3-9447-4176-B313-BF8025FD8D09';
728
-
729
- const creationMode = agent.ArtifactCreationMode;
730
- if (creationMode === 'System Only') {
731
- artifact.Visibility = 'System Only';
732
- LogStatus(`Task artifact marked as "System Only" per agent configuration`);
733
- } else {
734
- artifact.Visibility = 'Always';
735
- }
736
-
737
- if (!await artifact.Save()) {
738
- throw new Error(`Failed to save artifact: ${artifact.LatestResult?.CompleteMessage ?? 'unknown error'}`);
739
- }
740
- LogStatus(`Created artifact: ${artifact.Name} (${artifact.ID})`);
741
-
742
- // Create Artifact Version with content
743
- const version = await md.GetEntityObject<MJArtifactVersionEntity>('MJ: Artifact Versions', this.contextUser);
744
- version.ArtifactID = artifact.ID;
745
- version.VersionNumber = 1;
746
- version.Content = output.type === 'message' ? output.content : JSON.stringify(output.content, null, 2);
747
- version.UserID = this.contextUser.ID;
748
-
749
- if (!await version.Save()) {
750
- throw new Error(`Failed to save artifact version: ${version.LatestResult?.CompleteMessage ?? 'unknown error'}`);
751
- }
752
- LogStatus(`Created artifact version: ${version.ID}`);
753
-
754
- // If extraction produced a better name, update the artifact in the same transaction
755
- const nameAttr = (version as any).Attributes?.find((attr: any) =>
756
- attr.StandardProperty === 'name' || attr.Name?.toLowerCase() === 'name'
757
- );
758
-
759
- let extractedName = nameAttr?.Value?.trim();
760
- if (extractedName && extractedName.toLowerCase() !== 'null') {
761
- extractedName = extractedName.replace(/^["']|["']$/g, '');
762
- artifact.Name = extractedName;
763
- if (!await artifact.Save()) {
764
- throw new Error(`Failed to update artifact name: ${artifact.LatestResult?.CompleteMessage ?? 'unknown error'}`);
765
- }
766
- LogStatus(`✨ Updated artifact name to: ${artifact.Name}`);
767
- }
768
-
769
- // Create M2M relationship linking artifact to conversation detail
770
- const junction = await md.GetEntityObject<MJConversationDetailArtifactEntity>(
771
- 'MJ: Conversation Detail Artifacts',
772
- this.contextUser
773
- );
774
- junction.ConversationDetailID = conversationDetailId;
775
- junction.ArtifactVersionID = version.ID;
776
- junction.Direction = 'Output';
777
-
778
- if (!await junction.Save()) {
779
- throw new Error(`Failed to create artifact-conversation association: ${junction.LatestResult?.CompleteMessage ?? 'unknown error'}`);
780
- }
781
- LogStatus(`Linked artifact ${artifact.ID} to conversation detail ${conversationDetailId}`);
782
-
783
- await provider.CommitTransaction();
784
- } catch (error) {
785
- await provider.RollbackTransaction();
786
- LogError(`Error creating artifact from output — all changes rolled back: ${error}`);
787
- }
788
- }
789
-
790
- /**
791
- * Create user notification for task graph completion
792
- * Notifies user that their multi-step workflow has completed
793
- */
794
- private async createTaskGraphCompletionNotification(parentTask: MJTaskEntity): Promise<void> {
795
- try {
796
- if (!parentTask.ConversationDetailID) {
797
- LogStatus('Skipping notification - no conversation detail linked');
798
- return;
799
- }
800
-
801
- const md = this.getMetadata();
802
-
803
- // Load conversation detail to get conversation ID
804
- const detail = await md.GetEntityObject<MJConversationDetailEntity>(
805
- 'MJ: Conversation Details',
806
- this.contextUser
807
- );
808
- if (!(await detail.Load(parentTask.ConversationDetailID))) {
809
- throw new Error(`Failed to load conversation detail ${parentTask.ConversationDetailID}`);
810
- }
811
-
812
- // Count child tasks and success rate
813
- const rv = new RunView();
814
- const tasksResult = await rv.RunView<MJTaskEntity>({
815
- EntityName: 'MJ: Tasks',
816
- ExtraFilter: `ParentID='${parentTask.ID}'`,
817
- ResultType: 'entity_object'
818
- }, this.contextUser);
819
-
820
- const childTasks = tasksResult.Success ? (tasksResult.Results || []) : [];
821
- const successCount = childTasks.filter(t => t.Status === 'Complete').length;
822
- const totalCount = childTasks.length;
823
-
824
- // Create notification
825
- const notification = await md.GetEntityObject<MJUserNotificationEntity>(
826
- 'MJ: User Notifications',
827
- this.contextUser
828
- );
829
-
830
- notification.UserID = this.contextUser.ID;
831
- notification.Title = `Workflow "${parentTask.Name}" completed`;
832
- notification.Message = `Your ${totalCount}-step workflow has finished. ${successCount} of ${totalCount} tasks completed successfully.`;
833
-
834
- // Navigation configuration
835
- notification.ResourceConfiguration = JSON.stringify({
836
- type: 'conversation',
837
- conversationId: detail.ConversationID,
838
- messageId: parentTask.ConversationDetailID,
839
- taskId: parentTask.ID
840
- });
841
-
842
- notification.Unread = true;
843
-
844
- if (!(await notification.Save())) {
845
- throw new Error('Failed to save notification');
846
- }
847
-
848
- LogStatus(`📬 Created task graph notification ${notification.ID} for user ${this.contextUser.ID}`);
849
-
850
- // Publish real-time event if pubSub available
851
- if (this.pubSub && this.userPayload) {
852
- // NOTE (B49): normalized from a malformed no-sessionId payload
853
- publishStatusUpdate(this.pubSub, {
854
- sessionId: this.userPayload.sessionId,
855
- ownerUserId: this.userPayload.userRecord.ID,
856
- message: JSON.stringify({
857
- type: 'notification',
858
- notificationId: notification.ID,
859
- action: 'create',
860
- title: notification.Title,
861
- message: notification.Message
862
- })
863
- });
864
-
865
- LogStatus(`📡 Published task graph notification event to client`);
866
- }
867
-
868
- } catch (error) {
869
- LogError(`Failed to create task graph notification: ${(error as Error).message}`);
870
- // Don't throw - notification failure shouldn't fail the task
871
- }
872
- }
873
- }