@memberjunction/server 5.2.0 → 5.3.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.
@@ -433,9 +433,6 @@ export class RunAIAgentResolver extends ResolverBase {
433
433
 
434
434
  const executionTime = Date.now() - startTime;
435
435
 
436
- // Publish final events
437
- this.publishFinalEvents(pubSub, sessionId, userPayload, result);
438
-
439
436
  // Create notification if enabled and artifact was created successfully
440
437
  if (createNotification && result.success && artifactInfo && artifactInfo.artifactId && artifactInfo.versionId && artifactInfo.versionNumber) {
441
438
  await this.createCompletionNotification(
@@ -456,6 +453,9 @@ export class RunAIAgentResolver extends ResolverBase {
456
453
  const sanitizedResult = this.sanitizeAgentResult(result);
457
454
  const returnResult = JSON.stringify(sanitizedResult);
458
455
 
456
+ // Publish final events with enriched result data for fire-and-forget clients
457
+ this.publishFinalEvents(pubSub, sessionId, userPayload, result, returnResult);
458
+
459
459
  // Log completion
460
460
  if (result.success) {
461
461
  LogStatus(`=== AI AGENT RUN COMPLETED FOR: ${agentEntity.Name} (${executionTime}ms) ===`);
@@ -491,9 +491,17 @@ export class RunAIAgentResolver extends ResolverBase {
491
491
  }
492
492
 
493
493
  /**
494
- * Publish final streaming events (partial result and completion)
494
+ * Publish final streaming events (partial result and completion).
495
+ * The completion event includes the full result JSON so clients using
496
+ * fire-and-forget mode can receive the result via WebSocket.
495
497
  */
496
- private publishFinalEvents(pubSub: PubSubEngine, sessionId: string, userPayload: UserPayload, result: ExecuteAgentResult) {
498
+ private publishFinalEvents(
499
+ pubSub: PubSubEngine,
500
+ sessionId: string,
501
+ userPayload: UserPayload,
502
+ result: ExecuteAgentResult,
503
+ resultJson?: string
504
+ ) {
497
505
  if (result.agentRun) {
498
506
  // Get the last step from agent run
499
507
  let lastStep = 'Completed';
@@ -519,15 +527,19 @@ export class RunAIAgentResolver extends ResolverBase {
519
527
  this.PublishStreamingUpdate(pubSub, partialMsg, userPayload);
520
528
  }
521
529
 
522
- // Publish completion with conversationDetailId for client-side routing
523
- const completeMsg: AgentExecutionStreamMessage = {
530
+ // Publish completion with conversationDetailId for client-side routing.
531
+ // Include result data so fire-and-forget clients can receive the full result via WebSocket.
532
+ const completionData: Record<string, unknown> = {
524
533
  sessionId,
525
534
  agentRunId: result.agentRun?.ID || 'unknown',
526
535
  type: 'complete',
527
536
  timestamp: new Date(),
528
- conversationDetailId: result.agentRun?.ConversationDetailID
537
+ conversationDetailId: result.agentRun?.ConversationDetailID,
538
+ success: result.success,
539
+ errorMessage: result.agentRun?.ErrorMessage || undefined,
540
+ result: resultJson || undefined
529
541
  };
530
- this.PublishStreamingUpdate(pubSub, completeMsg, userPayload);
542
+ this.PublishStreamingUpdate(pubSub, completionData, userPayload);
531
543
  }
532
544
 
533
545
  /**
@@ -742,7 +754,8 @@ export class RunAIAgentResolver extends ResolverBase {
742
754
  @Arg('createArtifacts', { nullable: true }) createArtifacts?: boolean,
743
755
  @Arg('createNotification', { nullable: true }) createNotification?: boolean,
744
756
  @Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
745
- @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
757
+ @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string,
758
+ @Arg('fireAndForget', { nullable: true }) fireAndForget?: boolean
746
759
  ): Promise<AIAgentRunResult> {
747
760
  // Check API key scope authorization for agent execution
748
761
  await this.CheckAPIKeyScopeAuthorization('agent:execute', agentId, userPayload);
@@ -769,7 +782,25 @@ export class RunAIAgentResolver extends ResolverBase {
769
782
  // Convert to JSON string for the existing executeAIAgent method
770
783
  const messagesJson = JSON.stringify(messages);
771
784
 
772
- // Delegate to existing implementation
785
+ if (fireAndForget) {
786
+ // Fire-and-forget mode: start execution in background, return immediately.
787
+ // The client will receive the result via WebSocket PubSub completion event.
788
+ this.executeAgentInBackground(
789
+ p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
790
+ data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
791
+ conversationDetailId, createArtifacts || false, createNotification || false,
792
+ sourceArtifactId, sourceArtifactVersionId
793
+ );
794
+
795
+ LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
796
+
797
+ return {
798
+ success: true,
799
+ result: JSON.stringify({ accepted: true, fireAndForget: true })
800
+ };
801
+ }
802
+
803
+ // Synchronous mode (default): wait for execution to complete
773
804
  return this.executeAIAgent(
774
805
  p,
775
806
  dataSource,
@@ -801,6 +832,58 @@ export class RunAIAgentResolver extends ResolverBase {
801
832
  }
802
833
  }
803
834
 
835
+ /**
836
+ * Execute agent in background (fire-and-forget).
837
+ * Handles errors by publishing error completion events via PubSub,
838
+ * so the client receives them via WebSocket even though the HTTP response
839
+ * has already been sent.
840
+ */
841
+ private executeAgentInBackground(
842
+ p: DatabaseProviderBase,
843
+ dataSource: unknown,
844
+ agentId: string,
845
+ userPayload: UserPayload,
846
+ messagesJson: string,
847
+ sessionId: string,
848
+ pubSub: PubSubEngine,
849
+ data?: string,
850
+ payload?: string,
851
+ lastRunId?: string,
852
+ autoPopulateLastRunPayload?: boolean,
853
+ configurationId?: string,
854
+ conversationDetailId?: string,
855
+ createArtifacts: boolean = false,
856
+ createNotification: boolean = false,
857
+ sourceArtifactId?: string,
858
+ sourceArtifactVersionId?: string
859
+ ): void {
860
+ // Execute in background - errors are handled within, not propagated
861
+ this.executeAIAgent(
862
+ p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
863
+ data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
864
+ configurationId, conversationDetailId, createArtifacts, createNotification,
865
+ sourceArtifactId, sourceArtifactVersionId
866
+ ).catch((error: unknown) => {
867
+ // Background execution failed unexpectedly (executeAIAgent has its own try-catch,
868
+ // so this would only fire for truly unexpected errors).
869
+ const errorMessage = (error instanceof Error) ? error.message : 'Unknown background execution error';
870
+ LogError(`🔥 Fire-and-forget background execution failed: ${errorMessage}`, undefined, error);
871
+
872
+ // Publish error completion event so the client knows the agent failed
873
+ const errorCompletionData: Record<string, unknown> = {
874
+ sessionId,
875
+ agentRunId: 'unknown',
876
+ type: 'complete',
877
+ timestamp: new Date(),
878
+ conversationDetailId,
879
+ success: false,
880
+ errorMessage,
881
+ result: JSON.stringify({ success: false, errorMessage })
882
+ };
883
+ this.PublishStreamingUpdate(pubSub, errorCompletionData, userPayload);
884
+ });
885
+ }
886
+
804
887
  /**
805
888
  * Load conversation history with attachments from database.
806
889
  * Builds ChatMessage[] with multimodal content blocks for attachments.
@@ -16,7 +16,7 @@ import { LogError, LogStatus } from '@memberjunction/core';
16
16
  import { TestEngine } from '@memberjunction/testing-engine';
17
17
  import { ResolverBase } from '../generic/ResolverBase.js';
18
18
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
19
- import { TestRunVariables } from '@memberjunction/testing-engine-base';
19
+ import { TestRunVariables, TestLogMessage } from '@memberjunction/testing-engine-base';
20
20
 
21
21
  // ===== GraphQL Types =====
22
22
 
@@ -131,6 +131,11 @@ export class RunTestResolver extends ResolverBase {
131
131
  this.createProgressCallback(pubSub, userPayload, testId) :
132
132
  undefined;
133
133
 
134
+ // Create log callback to stream driver/engine logs to the UI in real-time
135
+ const logCallback = pubSub ?
136
+ this.createLogCallback(pubSub, userPayload, testId) :
137
+ undefined;
138
+
134
139
  // Parse variables from JSON string if provided
135
140
  let parsedVariables: TestRunVariables | undefined;
136
141
  if (variables) {
@@ -147,7 +152,8 @@ export class RunTestResolver extends ResolverBase {
147
152
  environment,
148
153
  tags,
149
154
  variables: parsedVariables,
150
- progressCallback
155
+ progressCallback,
156
+ logCallback
151
157
  };
152
158
 
153
159
  const result = await engine.RunTest(testId, options, user);
@@ -251,6 +257,11 @@ export class RunTestResolver extends ResolverBase {
251
257
  this.createProgressCallback(pubSub, userPayload, suiteId) :
252
258
  undefined;
253
259
 
260
+ // Create log callback to stream driver/engine logs to the UI in real-time
261
+ const logCallback = pubSub ?
262
+ this.createLogCallback(pubSub, userPayload, suiteId) :
263
+ undefined;
264
+
254
265
  // Parse selectedTestIds from JSON string if provided
255
266
  let parsedSelectedTestIds: string[] | undefined;
256
267
  if (selectedTestIds) {
@@ -280,7 +291,8 @@ export class RunTestResolver extends ResolverBase {
280
291
  selectedTestIds: parsedSelectedTestIds,
281
292
  sequenceStart,
282
293
  sequenceEnd,
283
- progressCallback
294
+ progressCallback,
295
+ logCallback
284
296
  };
285
297
 
286
298
  const result = await engine.RunSuite(suiteId, options, user);
@@ -364,6 +376,32 @@ export class RunTestResolver extends ResolverBase {
364
376
  };
365
377
  }
366
378
 
379
+ /**
380
+ * Create log callback that streams driver/engine log messages to the UI
381
+ * as progress updates, so they appear in the execution log in real-time.
382
+ */
383
+ private createLogCallback(
384
+ pubSub: PubSubEngine,
385
+ userPayload: UserPayload,
386
+ testId: string
387
+ ) {
388
+ return (message: TestLogMessage) => {
389
+ const progressMsg: TestExecutionStreamMessage = {
390
+ sessionId: userPayload.sessionId || '',
391
+ testRunId: testId,
392
+ type: 'progress',
393
+ progress: {
394
+ currentStep: 'driver_log',
395
+ percentage: -1, // Signal that percentage should not be updated
396
+ message: message.message,
397
+ },
398
+ timestamp: message.timestamp
399
+ };
400
+
401
+ this.publishProgress(pubSub, progressMsg, userPayload);
402
+ };
403
+ }
404
+
367
405
  private publishProgress(pubSub: PubSubEngine, data: TestExecutionStreamMessage, userPayload: UserPayload) {
368
406
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
369
407
  message: JSON.stringify({