@memberjunction/server 2.109.0 → 2.110.1

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 (35) hide show
  1. package/dist/agents/skip-agent.d.ts +1 -0
  2. package/dist/agents/skip-agent.d.ts.map +1 -1
  3. package/dist/agents/skip-agent.js +65 -7
  4. package/dist/agents/skip-agent.js.map +1 -1
  5. package/dist/agents/skip-sdk.d.ts.map +1 -1
  6. package/dist/agents/skip-sdk.js +29 -14
  7. package/dist/agents/skip-sdk.js.map +1 -1
  8. package/dist/generated/generated.d.ts +34 -13
  9. package/dist/generated/generated.d.ts.map +1 -1
  10. package/dist/generated/generated.js +194 -86
  11. package/dist/generated/generated.js.map +1 -1
  12. package/dist/resolvers/CreateQueryResolver.d.ts +1 -0
  13. package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
  14. package/dist/resolvers/CreateQueryResolver.js +73 -11
  15. package/dist/resolvers/CreateQueryResolver.js.map +1 -1
  16. package/dist/resolvers/RunAIAgentResolver.d.ts +6 -2
  17. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  18. package/dist/resolvers/RunAIAgentResolver.js +234 -8
  19. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  20. package/dist/resolvers/TaskResolver.d.ts +1 -1
  21. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  22. package/dist/resolvers/TaskResolver.js +4 -3
  23. package/dist/resolvers/TaskResolver.js.map +1 -1
  24. package/dist/services/TaskOrchestrator.d.ts +3 -1
  25. package/dist/services/TaskOrchestrator.d.ts.map +1 -1
  26. package/dist/services/TaskOrchestrator.js +77 -2
  27. package/dist/services/TaskOrchestrator.js.map +1 -1
  28. package/package.json +35 -35
  29. package/src/agents/skip-agent.ts +93 -9
  30. package/src/agents/skip-sdk.ts +45 -16
  31. package/src/generated/generated.ts +135 -69
  32. package/src/resolvers/CreateQueryResolver.ts +125 -28
  33. package/src/resolvers/RunAIAgentResolver.ts +397 -9
  34. package/src/resolvers/TaskResolver.ts +3 -2
  35. package/src/services/TaskOrchestrator.ts +118 -3
@@ -1,7 +1,7 @@
1
1
  import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine, Subscription, Root, ResolverFilterData, ID } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
- import { DatabaseProviderBase, LogError, LogStatus } from '@memberjunction/core';
4
- import { AIAgentEntityExtended } from '@memberjunction/core-entities';
3
+ import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
+ import { AIAgentEntityExtended, ArtifactEntity, ArtifactVersionEntity, ConversationDetailArtifactEntity, ConversationDetailEntity, UserNotificationEntity, AIAgentRunEntityExtended } from '@memberjunction/core-entities';
5
5
  import { AgentRunner } from '@memberjunction/ai-agents';
6
6
  import { ExecuteAgentResult } from '@memberjunction/ai-core-plus';
7
7
  import { AIEngine } from '@memberjunction/aiengine';
@@ -314,12 +314,16 @@ export class RunAIAgentResolver extends ResolverBase {
314
314
  sessionId: string,
315
315
  pubSub: PubSubEngine,
316
316
  data?: string,
317
- payload?: string,
317
+ payload?: string,
318
318
  templateData?: string,
319
319
  lastRunId?: string,
320
320
  autoPopulateLastRunPayload?: boolean,
321
321
  configurationId?: string,
322
- conversationDetailId?: string
322
+ conversationDetailId?: string,
323
+ createArtifacts: boolean = false,
324
+ createNotification: boolean = false,
325
+ sourceArtifactId?: string,
326
+ sourceArtifactVersionId?: string
323
327
  ): Promise<AIAgentRunResult> {
324
328
  const startTime = Date.now();
325
329
 
@@ -380,6 +384,37 @@ export class RunAIAgentResolver extends ResolverBase {
380
384
  // Publish final events
381
385
  this.publishFinalEvents(pubSub, sessionId, userPayload, result);
382
386
 
387
+ // Process completion for artifacts and notifications (if enabled)
388
+ if (result.success && conversationDetailId && result.payload) {
389
+ const currentUser = this.GetUserFromPayload(userPayload);
390
+
391
+ if (createArtifacts) {
392
+ const artifactInfo = await this.processAgentCompletionForArtifacts(
393
+ result.agentRun,
394
+ result.payload,
395
+ currentUser,
396
+ conversationDetailId,
397
+ sourceArtifactId
398
+ );
399
+
400
+ // Create notification if enabled and artifact was created successfully
401
+ if (createNotification && artifactInfo.artifactId && artifactInfo.versionId && artifactInfo.versionNumber) {
402
+ await this.createCompletionNotification(
403
+ result.agentRun,
404
+ {
405
+ artifactId: artifactInfo.artifactId,
406
+ versionId: artifactInfo.versionId,
407
+ versionNumber: artifactInfo.versionNumber
408
+ },
409
+ conversationDetailId,
410
+ currentUser,
411
+ pubSub,
412
+ userPayload
413
+ );
414
+ }
415
+ }
416
+ }
417
+
383
418
  // Create sanitized payload for JSON serialization
384
419
  const sanitizedResult = this.sanitizeAgentResult(result);
385
420
  const returnResult = JSON.stringify(sanitizedResult);
@@ -473,7 +508,11 @@ export class RunAIAgentResolver extends ResolverBase {
473
508
  @Arg('lastRunId', { nullable: true }) lastRunId?: string,
474
509
  @Arg('autoPopulateLastRunPayload', { nullable: true }) autoPopulateLastRunPayload?: boolean,
475
510
  @Arg('configurationId', { nullable: true }) configurationId?: string,
476
- @Arg('conversationDetailId', { nullable: true }) conversationDetailId?: string
511
+ @Arg('conversationDetailId', { nullable: true }) conversationDetailId?: string,
512
+ @Arg('createArtifacts', { nullable: true }) createArtifacts?: boolean,
513
+ @Arg('createNotification', { nullable: true }) createNotification?: boolean,
514
+ @Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
515
+ @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
477
516
  ): Promise<AIAgentRunResult> {
478
517
  const p = GetReadWriteProvider(providers);
479
518
  return this.executeAIAgent(
@@ -489,7 +528,11 @@ export class RunAIAgentResolver extends ResolverBase {
489
528
  lastRunId,
490
529
  autoPopulateLastRunPayload,
491
530
  configurationId,
492
- conversationDetailId
531
+ conversationDetailId,
532
+ createArtifacts || false,
533
+ createNotification || false,
534
+ sourceArtifactId,
535
+ sourceArtifactVersionId
493
536
  );
494
537
  }
495
538
 
@@ -511,7 +554,11 @@ export class RunAIAgentResolver extends ResolverBase {
511
554
  @Arg('lastRunId', { nullable: true }) lastRunId?: string,
512
555
  @Arg('autoPopulateLastRunPayload', { nullable: true }) autoPopulateLastRunPayload?: boolean,
513
556
  @Arg('configurationId', { nullable: true }) configurationId?: string,
514
- @Arg('conversationDetailId', { nullable: true }) conversationDetailId?: string
557
+ @Arg('conversationDetailId', { nullable: true }) conversationDetailId?: string,
558
+ @Arg('createArtifacts', { nullable: true }) createArtifacts?: boolean,
559
+ @Arg('createNotification', { nullable: true }) createNotification?: boolean,
560
+ @Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
561
+ @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
515
562
  ): Promise<AIAgentRunResult> {
516
563
  const p = GetReadWriteProvider(providers);
517
564
  return this.executeAIAgent(
@@ -527,8 +574,349 @@ export class RunAIAgentResolver extends ResolverBase {
527
574
  lastRunId,
528
575
  autoPopulateLastRunPayload,
529
576
  configurationId,
530
- conversationDetailId
577
+ conversationDetailId,
578
+ createArtifacts || false,
579
+ createNotification || false,
580
+ sourceArtifactId,
581
+ sourceArtifactVersionId
531
582
  );
532
583
  }
533
-
584
+
585
+ /**
586
+ * Get the maximum version number for an artifact
587
+ * Used when creating new version of an explicitly specified artifact
588
+ */
589
+ private async getMaxVersionForArtifact(
590
+ artifactId: string,
591
+ contextUser: UserInfo
592
+ ): Promise<number> {
593
+ try {
594
+ const rv = new RunView();
595
+
596
+ // Query all versions for this artifact to find max version number
597
+ const result = await rv.RunView<ArtifactVersionEntity>({
598
+ EntityName: 'MJ: Artifact Versions',
599
+ ExtraFilter: `ArtifactID='${artifactId}'`,
600
+ OrderBy: 'VersionNumber DESC',
601
+ MaxRows: 1,
602
+ ResultType: 'entity_object'
603
+ }, contextUser);
604
+
605
+ if (result.Success && result.Results && result.Results.length > 0) {
606
+ return result.Results[0].VersionNumber || 0;
607
+ }
608
+
609
+ return 0; // No versions found, will create version 1
610
+ } catch (error) {
611
+ LogError(`Error getting max version for artifact: ${(error as Error).message}`);
612
+ return 0;
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Find the most recent artifact for a conversation detail to determine versioning
618
+ * Returns artifact info if exists, null if this is first artifact
619
+ */
620
+ private async findPreviousArtifactForMessage(
621
+ conversationDetailId: string,
622
+ contextUser: UserInfo
623
+ ): Promise<{ artifactId: string; versionNumber: number } | null> {
624
+ try {
625
+ const rv = new RunView();
626
+
627
+ // Query junction table to find artifacts for this message
628
+ const result = await rv.RunView<ConversationDetailArtifactEntity>({
629
+ EntityName: 'MJ: Conversation Detail Artifacts',
630
+ ExtraFilter: `ConversationDetailID='${conversationDetailId}' AND Direction='Output'`,
631
+ OrderBy: '__mj_CreatedAt DESC',
632
+ MaxRows: 1,
633
+ ResultType: 'entity_object'
634
+ }, contextUser);
635
+
636
+ if (!result.Success || !result.Results || result.Results.length === 0) {
637
+ return null;
638
+ }
639
+
640
+ const junction = result.Results[0];
641
+
642
+ // Load the artifact version to get version number and artifact ID
643
+ const md = new Metadata();
644
+ const version = await md.GetEntityObject<ArtifactVersionEntity>(
645
+ 'MJ: Artifact Versions',
646
+ contextUser
647
+ );
648
+
649
+ if (!(await version.Load(junction.ArtifactVersionID))) {
650
+ return null;
651
+ }
652
+
653
+ return {
654
+ artifactId: version.ArtifactID,
655
+ versionNumber: version.VersionNumber
656
+ };
657
+ } catch (error) {
658
+ LogError(`Error finding previous artifact: ${(error as Error).message}`);
659
+ return null;
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Process agent completion to create artifacts from payload
665
+ * Called after agent run completes successfully
666
+ */
667
+ private async processAgentCompletionForArtifacts(
668
+ agentRun: AIAgentRunEntityExtended,
669
+ payload: any,
670
+ contextUser: UserInfo,
671
+ conversationDetailId?: string,
672
+ sourceArtifactId?: string
673
+ ): Promise<{ artifactId?: string; versionId?: string; versionNumber?: number }> {
674
+ // Validate inputs
675
+ if (!payload || Object.keys(payload).length === 0) {
676
+ LogStatus('No payload to create artifact from');
677
+ return {};
678
+ }
679
+
680
+ if (!conversationDetailId) {
681
+ LogStatus('Skipping artifact creation - no conversationDetailId provided');
682
+ return {};
683
+ }
684
+
685
+ // Check agent's ArtifactCreationMode
686
+ await AIEngine.Instance.Config(false, contextUser);
687
+ const agent = AIEngine.Instance.Agents.find(a => a.ID === agentRun.AgentID);
688
+ const creationMode = agent?.ArtifactCreationMode;
689
+
690
+ if (creationMode === 'Never') {
691
+ LogStatus(`Skipping artifact creation - agent "${agent?.Name}" has ArtifactCreationMode='Never'`);
692
+ return {};
693
+ }
694
+
695
+ try {
696
+ const md = new Metadata();
697
+ const JSON_ARTIFACT_TYPE_ID = 'ae674c7e-ea0d-49ea-89e4-0649f5eb20d4';
698
+
699
+ // 1. Determine if creating new artifact or new version
700
+ let artifactId: string;
701
+ let newVersionNumber: number;
702
+ let isNewArtifact = false;
703
+
704
+ // Priority 1: Use explicit source artifact if provided (agent continuity/refinement)
705
+ if (sourceArtifactId) {
706
+ const maxVersion = await this.getMaxVersionForArtifact(sourceArtifactId, contextUser);
707
+ artifactId = sourceArtifactId;
708
+ newVersionNumber = maxVersion + 1;
709
+ LogStatus(`Creating version ${newVersionNumber} of source artifact ${artifactId} (explicit source)`);
710
+ }
711
+ // Priority 2: Try to find previous artifact for this message (fallback)
712
+ else {
713
+ const previousArtifact = await this.findPreviousArtifactForMessage(
714
+ conversationDetailId,
715
+ contextUser
716
+ );
717
+
718
+ if (previousArtifact) {
719
+ // Create new version of existing artifact
720
+ artifactId = previousArtifact.artifactId;
721
+ newVersionNumber = previousArtifact.versionNumber + 1;
722
+ LogStatus(`Creating version ${newVersionNumber} of existing artifact ${artifactId}`);
723
+ } else {
724
+ // Create new artifact header
725
+ const artifact = await md.GetEntityObject<ArtifactEntity>(
726
+ 'MJ: Artifacts',
727
+ contextUser
728
+ );
729
+
730
+ // Get agent info for naming and visibility control
731
+ await AIEngine.Instance.Config(false, contextUser);
732
+ const agent = AIEngine.Instance.Agents.find(a => a.ID === agentRun.AgentID);
733
+ const agentName = agent?.Name || 'Agent';
734
+
735
+ artifact.Name = `${agentName} Payload - ${new Date().toLocaleString()}`;
736
+ artifact.Description = `Payload returned by ${agentName}`;
737
+
738
+ // Use agent's DefaultArtifactTypeID if available, otherwise JSON
739
+ const defaultArtifactTypeId = (agent as any)?.DefaultArtifactTypeID;
740
+ artifact.TypeID = defaultArtifactTypeId || JSON_ARTIFACT_TYPE_ID;
741
+
742
+ artifact.UserID = contextUser.ID;
743
+ artifact.EnvironmentID = (contextUser as any).EnvironmentID ||
744
+ 'F51358F3-9447-4176-B313-BF8025FD8D09';
745
+
746
+ // Set visibility based on agent's ArtifactCreationMode
747
+ // Will compile after CodeGen adds the new fields
748
+ const creationMode = agent.ArtifactCreationMode;
749
+ if (creationMode === 'System Only') {
750
+ artifact.Visibility = 'System Only';
751
+ LogStatus(`Artifact marked as "System Only" per agent configuration`);
752
+ } else {
753
+ artifact.Visibility = 'Always';
754
+ }
755
+
756
+ if (!(await artifact.Save())) {
757
+ throw new Error('Failed to save artifact');
758
+ }
759
+
760
+ artifactId = artifact.ID;
761
+ newVersionNumber = 1;
762
+ isNewArtifact = true;
763
+ LogStatus(`Created new artifact: ${artifact.Name} (${artifactId})`);
764
+ }
765
+ }
766
+
767
+ // 2. Create artifact version with content
768
+ const version = await md.GetEntityObject<ArtifactVersionEntity>(
769
+ 'MJ: Artifact Versions',
770
+ contextUser
771
+ );
772
+ version.ArtifactID = artifactId;
773
+ version.VersionNumber = newVersionNumber;
774
+ version.Content = JSON.stringify(payload, null, 2);
775
+ version.UserID = contextUser.ID;
776
+
777
+ if (!(await version.Save())) {
778
+ throw new Error('Failed to save artifact version');
779
+ }
780
+
781
+ LogStatus(`Created artifact version ${newVersionNumber} (${version.ID})`);
782
+
783
+ // If this is the first version of a new artifact, check for extracted Name attribute and update artifact
784
+ if (isNewArtifact && newVersionNumber === 1) {
785
+ const nameAttr = (version as any).Attributes?.find((attr: any) =>
786
+ attr.StandardProperty === 'name' || attr.Name?.toLowerCase() === 'name'
787
+ );
788
+
789
+ // Check for valid name value (not null, not empty, not string "null")
790
+ let extractedName = nameAttr?.Value?.trim();
791
+ if (extractedName && extractedName.toLowerCase() !== 'null') {
792
+ // Strip surrounding quotes (double or single) from start and end
793
+ extractedName = extractedName.replace(/^["']|["']$/g, '');
794
+
795
+ // Load artifact to update with extracted name
796
+ const artifact = await md.GetEntityObject<ArtifactEntity>(
797
+ 'MJ: Artifacts',
798
+ contextUser
799
+ );
800
+
801
+ if (!(await artifact.Load(artifactId))) {
802
+ LogError('Failed to reload artifact for name update');
803
+ } else {
804
+ artifact.Name = extractedName;
805
+ if (await artifact.Save()) {
806
+ LogStatus(`✨ Updated artifact name to: ${artifact.Name}`);
807
+ }
808
+ }
809
+ }
810
+ }
811
+
812
+ // 3. Create junction record linking artifact to conversation detail
813
+ const junction = await md.GetEntityObject<ConversationDetailArtifactEntity>(
814
+ 'MJ: Conversation Detail Artifacts',
815
+ contextUser
816
+ );
817
+ junction.ConversationDetailID = conversationDetailId;
818
+ junction.ArtifactVersionID = version.ID;
819
+ junction.Direction = 'Output';
820
+
821
+ if (!(await junction.Save())) {
822
+ throw new Error('Failed to create artifact-message association');
823
+ }
824
+
825
+ LogStatus(`Linked artifact to conversation detail ${conversationDetailId}`);
826
+
827
+ return {
828
+ artifactId,
829
+ versionId: version.ID,
830
+ versionNumber: newVersionNumber
831
+ };
832
+ } catch (error) {
833
+ LogError(`Failed to process agent completion for artifacts: ${(error as Error).message}`);
834
+ return {};
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Create a user notification for agent completion with artifact
840
+ * Notification includes navigation link back to the conversation
841
+ */
842
+ private async createCompletionNotification(
843
+ agentRun: AIAgentRunEntityExtended,
844
+ artifactInfo: { artifactId: string; versionId: string; versionNumber: number },
845
+ conversationDetailId: string,
846
+ contextUser: UserInfo,
847
+ pubSub: PubSubEngine,
848
+ userPayload: UserPayload
849
+ ): Promise<void> {
850
+ try {
851
+ const md = new Metadata();
852
+
853
+ // Get agent info for notification message
854
+ await AIEngine.Instance.Config(false, contextUser);
855
+ const agent = AIEngine.Instance.Agents.find(a => a.ID === agentRun.AgentID);
856
+ const agentName = agent?.Name || 'Agent';
857
+
858
+ // Load conversation detail to get conversation info
859
+ const detail = await md.GetEntityObject<ConversationDetailEntity>(
860
+ 'Conversation Details',
861
+ contextUser
862
+ );
863
+ if (!(await detail.Load(conversationDetailId))) {
864
+ throw new Error(`Failed to load conversation detail ${conversationDetailId}`);
865
+ }
866
+
867
+ // Create notification entity
868
+ const notification = await md.GetEntityObject<UserNotificationEntity>(
869
+ 'User Notifications',
870
+ contextUser
871
+ );
872
+
873
+ notification.UserID = contextUser.ID;
874
+ notification.Title = `${agentName} completed your request`;
875
+
876
+ // Craft message based on versioning
877
+ if (artifactInfo.versionNumber > 1) {
878
+ notification.Message = `${agentName} has finished processing and created version ${artifactInfo.versionNumber}`;
879
+ } else {
880
+ notification.Message = `${agentName} has finished processing and created a new artifact`;
881
+ }
882
+
883
+ // Store navigation configuration as JSON
884
+ // Client will parse this to navigate to the conversation with artifact visible
885
+ notification.ResourceConfiguration = JSON.stringify({
886
+ type: 'conversation',
887
+ conversationId: detail.ConversationID,
888
+ messageId: conversationDetailId,
889
+ artifactId: artifactInfo.artifactId,
890
+ versionNumber: artifactInfo.versionNumber
891
+ });
892
+
893
+ notification.Unread = true; // Default unread
894
+ // ResourceTypeID and ResourceRecordID left null - using custom navigation
895
+
896
+ if (!(await notification.Save())) {
897
+ throw new Error('Failed to save notification');
898
+ }
899
+
900
+ LogStatus(`📬 Created notification ${notification.ID} for user ${contextUser.ID}`);
901
+
902
+ // Publish real-time notification event so client updates immediately
903
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
904
+ userPayload: JSON.stringify(userPayload),
905
+ message: JSON.stringify({
906
+ type: 'notification',
907
+ notificationId: notification.ID,
908
+ action: 'create',
909
+ title: notification.Title,
910
+ message: notification.Message
911
+ })
912
+ });
913
+
914
+ LogStatus(`📡 Published notification event to client`);
915
+
916
+ } catch (error) {
917
+ LogError(`Failed to create completion notification: ${(error as Error).message}`);
918
+ // Don't throw - notification failure shouldn't fail the agent run
919
+ }
920
+ }
921
+
534
922
  }
@@ -53,7 +53,8 @@ export class TaskOrchestrationResolver extends ResolverBase {
53
53
  @Arg('environmentId') environmentId: string,
54
54
  @Arg('sessionId') sessionId: string,
55
55
  @PubSub() pubSub: PubSubEngine,
56
- @Ctx() { userPayload }: AppContext
56
+ @Ctx() { userPayload }: AppContext,
57
+ @Arg('createNotifications', { nullable: true }) createNotifications?: boolean
57
58
  ): Promise<ExecuteTaskGraphResult> {
58
59
  try {
59
60
  LogStatus(`=== EXECUTING TASK GRAPH FOR CONVERSATION: ${conversationDetailId} ===`);
@@ -78,7 +79,7 @@ export class TaskOrchestrationResolver extends ResolverBase {
78
79
  }
79
80
 
80
81
  // Create task orchestrator with PubSub for progress updates
81
- const orchestrator = new TaskOrchestrator(currentUser, pubSub, sessionId, userPayload);
82
+ const orchestrator = new TaskOrchestrator(currentUser, pubSub, sessionId, userPayload, createNotifications || false);
82
83
 
83
84
  // Create parent task and child tasks with dependencies
84
85
  const { parentTaskId, taskIdMap } = await orchestrator.createTasksFromGraph(
@@ -1,5 +1,5 @@
1
1
  import { Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
2
- import { TaskEntity, TaskDependencyEntity, TaskTypeEntity, AIAgentEntityExtended, ConversationDetailEntity, ArtifactEntity, ArtifactVersionEntity, ConversationDetailArtifactEntity } from '@memberjunction/core-entities';
2
+ import { TaskEntity, TaskDependencyEntity, TaskTypeEntity, AIAgentEntityExtended, ConversationDetailEntity, ArtifactEntity, ArtifactVersionEntity, ConversationDetailArtifactEntity, UserNotificationEntity } from '@memberjunction/core-entities';
3
3
  import { AgentRunner } from '@memberjunction/ai-agents';
4
4
  import { ChatMessageRole } from '@memberjunction/ai';
5
5
  import { PubSubEngine } from 'type-graphql';
@@ -50,7 +50,8 @@ export class TaskOrchestrator {
50
50
  private contextUser: UserInfo,
51
51
  private pubSub?: PubSubEngine,
52
52
  private sessionId?: string,
53
- private userPayload?: UserPayload
53
+ private userPayload?: UserPayload,
54
+ private createNotifications: boolean = false
54
55
  ) {}
55
56
 
56
57
  /**
@@ -408,9 +409,14 @@ export class TaskOrchestrator {
408
409
  parentTask.Status = 'Complete';
409
410
  parentTask.PercentComplete = 100;
410
411
  parentTask.CompletedAt = new Date();
411
- await parentTask.Save();
412
+ const saved = await parentTask.Save();
412
413
 
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
+ }
414
420
  }
415
421
 
416
422
  /**
@@ -703,6 +709,16 @@ export class TaskOrchestrator {
703
709
  artifact.UserID = this.contextUser.ID;
704
710
  artifact.EnvironmentID = (this.contextUser as any).EnvironmentID || 'F51358F3-9447-4176-B313-BF8025FD8D09';
705
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
+
706
722
  const artifactSaved = await artifact.Save();
707
723
  if (!artifactSaved) {
708
724
  LogError('Failed to save artifact');
@@ -733,6 +749,23 @@ export class TaskOrchestrator {
733
749
 
734
750
  LogStatus(`Created artifact version: ${version.ID}`);
735
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
+
736
769
  // Create M2M relationship linking artifact to conversation detail
737
770
  const junction = await md.GetEntityObject<ConversationDetailArtifactEntity>(
738
771
  'MJ: Conversation Detail Artifacts',
@@ -753,4 +786,86 @@ export class TaskOrchestrator {
753
786
  LogError(`Error creating artifact from output: ${error}`);
754
787
  }
755
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: 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
+ }
870
+ }
756
871
  }