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