@memberjunction/server 5.10.1 → 5.12.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 (40) hide show
  1. package/dist/agents/skip-sdk.d.ts.map +1 -1
  2. package/dist/agents/skip-sdk.js +1 -0
  3. package/dist/agents/skip-sdk.js.map +1 -1
  4. package/dist/generated/generated.d.ts +220 -0
  5. package/dist/generated/generated.d.ts.map +1 -1
  6. package/dist/generated/generated.js +1516 -285
  7. package/dist/generated/generated.js.map +1 -1
  8. package/dist/generic/ResolverBase.js +3 -3
  9. package/dist/generic/ResolverBase.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +29 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  14. package/dist/resolvers/AdhocQueryResolver.js +8 -0
  15. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  16. package/dist/resolvers/CreateQueryResolver.d.ts +2 -0
  17. package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
  18. package/dist/resolvers/CreateQueryResolver.js +11 -0
  19. package/dist/resolvers/CreateQueryResolver.js.map +1 -1
  20. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  21. package/dist/resolvers/GetDataResolver.js +16 -2
  22. package/dist/resolvers/GetDataResolver.js.map +1 -1
  23. package/dist/resolvers/QueryResolver.d.ts +2 -0
  24. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  25. package/dist/resolvers/QueryResolver.js +20 -0
  26. package/dist/resolvers/QueryResolver.js.map +1 -1
  27. package/dist/resolvers/RunAIAgentResolver.d.ts +24 -0
  28. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  29. package/dist/resolvers/RunAIAgentResolver.js +264 -1
  30. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  31. package/package.json +59 -59
  32. package/src/agents/skip-sdk.ts +1 -0
  33. package/src/generated/generated.ts +1135 -286
  34. package/src/generic/ResolverBase.ts +3 -3
  35. package/src/index.ts +31 -2
  36. package/src/resolvers/AdhocQueryResolver.ts +8 -0
  37. package/src/resolvers/CreateQueryResolver.ts +9 -0
  38. package/src/resolvers/GetDataResolver.ts +18 -2
  39. package/src/resolvers/QueryResolver.ts +18 -0
  40. package/src/resolvers/RunAIAgentResolver.ts +301 -2
@@ -1061,7 +1061,7 @@ export class ResolverBase {
1061
1061
  if (await entityObject.Save()) {
1062
1062
  // save worked, fire the AfterCreate event and then return all the data
1063
1063
  await this.AfterCreate(provider, input); // fire event
1064
- this.PublishCacheInvalidation(entityObject, 'save', userPayload);
1064
+ // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1065
1065
  const contextUser = this.GetUserFromPayload(userPayload);
1066
1066
  // MapFieldNamesToCodeNames now handles encryption filtering as well
1067
1067
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
@@ -1145,7 +1145,7 @@ export class ResolverBase {
1145
1145
  if (await entityObject.Save()) {
1146
1146
  // save worked, fire afterevent and return all the data
1147
1147
  await this.AfterUpdate(provider, input); // fire event
1148
- this.PublishCacheInvalidation(entityObject, 'save', userPayload);
1148
+ // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1149
1149
 
1150
1150
  // MapFieldNamesToCodeNames now handles encryption filtering as well
1151
1151
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
@@ -1372,7 +1372,7 @@ export class ResolverBase {
1372
1372
 
1373
1373
  if (await entityObject.Delete(options)) {
1374
1374
  await this.AfterDelete(provider, key); // fire event
1375
- this.PublishCacheInvalidation(entityObject, 'delete', userPayload);
1375
+ // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1376
1376
  return returnValue;
1377
1377
  } else {
1378
1378
  throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
package/src/index.ts CHANGED
@@ -4,8 +4,8 @@ dotenv.config({ quiet: true });
4
4
 
5
5
  import { expressMiddleware } from '@as-integrations/express5';
6
6
  import { mergeSchemas } from '@graphql-tools/schema';
7
- import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport } from '@memberjunction/core';
8
- import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
7
+ import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent } from '@memberjunction/core';
8
+ import { MJGlobal, MJEventType, UUIDsEqual } from '@memberjunction/global';
9
9
  import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
10
10
  import { extendConnectionPoolWithQuery } from './util.js';
11
11
  import { default as BodyParser } from 'body-parser';
@@ -426,6 +426,28 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
426
426
  }
427
427
  PubSubManager.Instance.SetPubSubEngine(pubSub as unknown as PubSubEngine);
428
428
 
429
+ // Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
430
+ // ANY BaseEntity save/delete occurs on this server — regardless of whether it
431
+ // originated from a GraphQL mutation or internal server-side code (agents, actions,
432
+ // task orchestrator, etc.). This closes the gap where server-internal saves were
433
+ // invisible to browser BaseEngine caches.
434
+ MJGlobal.Instance.GetEventListener(false).subscribe((event) => {
435
+ if (event.event === MJEventType.ComponentEvent && event.eventCode === BaseEntity.BaseEventCode) {
436
+ const beEvent = event.args as BaseEntityEvent;
437
+ if (beEvent.type === 'save' || beEvent.type === 'delete') {
438
+ PubSubManager.Instance.Publish(CACHE_INVALIDATION_TOPIC, {
439
+ entityName: beEvent.baseEntity.EntityInfo.Name,
440
+ primaryKeyValues: JSON.stringify(beEvent.baseEntity.PrimaryKey.KeyValuePairs),
441
+ action: beEvent.type,
442
+ sourceServerId: MJGlobal.Instance.ProcessUUID,
443
+ timestamp: new Date(),
444
+ originSessionId: null,
445
+ recordData: beEvent.type === 'save' ? JSON.stringify(beEvent.baseEntity.GetAll()) : undefined,
446
+ });
447
+ }
448
+ }
449
+ });
450
+
429
451
  let schema = mergeSchemas({
430
452
  schemas: [
431
453
  buildSchemaSync({
@@ -505,6 +527,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
505
527
  level: 6
506
528
  }));
507
529
 
530
+ // Health check endpoint - registered before auth middleware so cloud
531
+ // platform probes (Azure App Service, AWS ALB, k8s, etc.) don't
532
+ // generate noisy auth errors in the logs.
533
+ app.get('/healthcheck', (_req, res) => {
534
+ res.status(200).json({ status: 'ok' });
535
+ });
536
+
508
537
  // Apply user-provided Express middleware (after compression, before routes)
509
538
  if (options?.ExpressMiddlewareBefore) {
510
539
  for (const mw of options.ExpressMiddlewareBefore) {
@@ -76,6 +76,8 @@ export class AdhocQueryResolver extends ResolverBase {
76
76
  Results: JSON.stringify(result.recordset ?? []),
77
77
  RowCount: result.recordset?.length ?? 0,
78
78
  TotalRowCount: result.recordset?.length ?? 0,
79
+ PageNumber: undefined,
80
+ PageSize: undefined,
79
81
  ExecutionTime: executionTimeMs,
80
82
  ErrorMessage: ''
81
83
  };
@@ -92,6 +94,8 @@ export class AdhocQueryResolver extends ResolverBase {
92
94
  Results: '[]',
93
95
  RowCount: 0,
94
96
  TotalRowCount: 0,
97
+ PageNumber: undefined,
98
+ PageSize: undefined,
95
99
  ExecutionTime: executionTimeMs,
96
100
  ErrorMessage: `Query execution exceeded ${input.TimeoutSeconds ?? 30} second timeout`
97
101
  };
@@ -105,6 +109,8 @@ export class AdhocQueryResolver extends ResolverBase {
105
109
  Results: '[]',
106
110
  RowCount: 0,
107
111
  TotalRowCount: 0,
112
+ PageNumber: undefined,
113
+ PageSize: undefined,
108
114
  ExecutionTime: executionTimeMs,
109
115
  ErrorMessage: `Query execution failed: ${errorMessage}`
110
116
  };
@@ -119,6 +125,8 @@ export class AdhocQueryResolver extends ResolverBase {
119
125
  Results: '[]',
120
126
  RowCount: 0,
121
127
  TotalRowCount: 0,
128
+ PageNumber: undefined,
129
+ PageSize: undefined,
122
130
  ExecutionTime: 0,
123
131
  ErrorMessage: errorMessage
124
132
  };
@@ -293,6 +293,9 @@ export class CreateQueryResultType {
293
293
  @Field(() => String, { nullable: true })
294
294
  EmbeddingModelName?: string;
295
295
 
296
+ @Field(() => String, { nullable: true })
297
+ TechnicalDescription?: string;
298
+
296
299
  // Related collections
297
300
  @Field(() => [QueryFieldType], { nullable: true })
298
301
  Fields?: QueryFieldType[];
@@ -349,6 +352,9 @@ export class UpdateQueryResultType {
349
352
  @Field(() => String, { nullable: true })
350
353
  EmbeddingModelName?: string;
351
354
 
355
+ @Field(() => String, { nullable: true })
356
+ TechnicalDescription?: string;
357
+
352
358
  // Related collections
353
359
  @Field(() => [QueryFieldType], { nullable: true })
354
360
  Fields?: QueryFieldType[];
@@ -477,6 +483,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
477
483
  EmbeddingVector: record.EmbeddingVector,
478
484
  EmbeddingModelID: record.EmbeddingModelID,
479
485
  EmbeddingModelName: record.EmbeddingModel,
486
+ TechnicalDescription: record.TechnicalDescription,
480
487
  Fields: record.QueryFields.map(f => ({
481
488
  ID: f.ID,
482
489
  QueryID: f.QueryID,
@@ -540,6 +547,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
540
547
  EmbeddingVector: existingQuery.EmbeddingVector,
541
548
  EmbeddingModelID: existingQuery.EmbeddingModelID,
542
549
  EmbeddingModelName: existingQuery.EmbeddingModel,
550
+ TechnicalDescription: existingQuery.TechnicalDescription,
543
551
  Fields: existingQuery.Fields?.map((f: any) => ({
544
552
  ID: f.ID,
545
553
  QueryID: f.QueryID,
@@ -736,6 +744,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
736
744
  EmbeddingVector: queryEntity.EmbeddingVector,
737
745
  EmbeddingModelID: queryEntity.EmbeddingModelID,
738
746
  EmbeddingModelName: queryEntity.EmbeddingModel,
747
+ TechnicalDescription: queryEntity.TechnicalDescription,
739
748
  Fields: queryEntity.QueryFields.map(f => ({
740
749
  ID: f.ID,
741
750
  QueryID: f.QueryID,
@@ -1,9 +1,11 @@
1
1
  import { Arg, Ctx, Field, InputType, ObjectType, Query } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { LogError, LogStatus, Metadata } from '@memberjunction/core';
3
+ import { LogError, LogStatus, Metadata, QueryCompositionEngine } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
  import { GetReadOnlyDataSource, GetReadOnlyProvider } from '../util.js';
7
+ import { getDbType } from '../index.js';
8
+ import { getSystemUser } from '../auth/index.js';
7
9
  import sql from 'mssql';
8
10
 
9
11
  @InputType()
@@ -128,13 +130,27 @@ export class GetDataResolver {
128
130
  throw new Error('Read-only data source not found');
129
131
  }
130
132
 
133
+ // Resolve composition tokens in queries before execution.
134
+ // Queries may contain {{query:"CategoryPath/QueryName(params)"}} tokens that
135
+ // reference reusable queries. These must be resolved to CTEs before raw SQL execution.
136
+ const compositionEngine = new QueryCompositionEngine();
137
+ const platform = getDbType();
138
+ const systemUser = await getSystemUser();
139
+
131
140
  // Execute all queries in parallel, but execute each individual query in its own try catch block so that if one fails, the others can still be processed
132
141
  // and also so that we can capture the error message for each query and return it
133
142
  const results = await Promise.allSettled(
134
143
  input.Queries.map(async (query) => {
135
144
  try {
145
+ // Resolve composition tokens if present
146
+ let resolvedSQL = query;
147
+ if (compositionEngine.HasCompositionTokens(query)) {
148
+ const compositionResult = compositionEngine.ResolveComposition(query, platform, systemUser);
149
+ resolvedSQL = compositionResult.ResolvedSQL;
150
+ }
151
+
136
152
  const request = new sql.Request(readOnlyDataSource);
137
- const result = await request.query(query);
153
+ const result = await request.query(resolvedSQL);
138
154
  return { result: result.recordset, error: null };
139
155
  } catch (err) {
140
156
  // Extract clean SQL error message
@@ -61,6 +61,12 @@ export class RunQueryResultType {
61
61
  @Field()
62
62
  TotalRowCount: number;
63
63
 
64
+ @Field(() => Int, { nullable: true })
65
+ PageNumber?: number;
66
+
67
+ @Field(() => Int, { nullable: true })
68
+ PageSize?: number;
69
+
64
70
  @Field()
65
71
  ExecutionTime: number;
66
72
 
@@ -233,6 +239,8 @@ export class RunQueryResolver extends ResolverBase {
233
239
  ExecutionTime: result.ExecutionTime ?? 0,
234
240
  ErrorMessage: result.ErrorMessage || '',
235
241
  AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
242
+ PageNumber: result.PageNumber,
243
+ PageSize: result.PageSize,
236
244
  CacheHit: (result as any).CacheHit,
237
245
  CacheTTLRemaining: (result as any).CacheTTLRemaining
238
246
  };
@@ -276,6 +284,8 @@ export class RunQueryResolver extends ResolverBase {
276
284
  ExecutionTime: result.ExecutionTime ?? 0,
277
285
  ErrorMessage: result.ErrorMessage || '',
278
286
  AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
287
+ PageNumber: result.PageNumber,
288
+ PageSize: result.PageSize,
279
289
  CacheHit: (result as any).CacheHit,
280
290
  CacheTTLRemaining: (result as any).CacheTTLRemaining
281
291
  };
@@ -332,6 +342,8 @@ export class RunQueryResolver extends ResolverBase {
332
342
  ExecutionTime: result.ExecutionTime ?? 0,
333
343
  ErrorMessage: result.ErrorMessage || '',
334
344
  AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
345
+ PageNumber: result.PageNumber,
346
+ PageSize: result.PageSize,
335
347
  CacheHit: (result as any).CacheHit,
336
348
  CacheTTLRemaining: (result as any).CacheTTLRemaining
337
349
  };
@@ -374,6 +386,8 @@ export class RunQueryResolver extends ResolverBase {
374
386
  ExecutionTime: result.ExecutionTime ?? 0,
375
387
  ErrorMessage: result.ErrorMessage || '',
376
388
  AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
389
+ PageNumber: result.PageNumber,
390
+ PageSize: result.PageSize,
377
391
  CacheHit: (result as any).CacheHit,
378
392
  CacheTTLRemaining: (result as any).CacheTTLRemaining
379
393
  };
@@ -424,6 +438,8 @@ export class RunQueryResolver extends ResolverBase {
424
438
  ExecutionTime: result.ExecutionTime ?? 0,
425
439
  ErrorMessage: result.ErrorMessage || '',
426
440
  AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
441
+ PageNumber: result.PageNumber,
442
+ PageSize: result.PageSize,
427
443
  CacheHit: (result as Record<string, unknown>).CacheHit as boolean | undefined,
428
444
  CacheTTLRemaining: (result as Record<string, unknown>).CacheTTLRemaining as number | undefined
429
445
  };
@@ -471,6 +487,8 @@ export class RunQueryResolver extends ResolverBase {
471
487
  ExecutionTime: result.ExecutionTime ?? 0,
472
488
  ErrorMessage: result.ErrorMessage || '',
473
489
  AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined,
490
+ PageNumber: result.PageNumber,
491
+ PageSize: result.PageSize,
474
492
  CacheHit: (result as Record<string, unknown>).CacheHit as boolean | undefined,
475
493
  CacheTTLRemaining: (result as Record<string, unknown>).CacheTTLRemaining as number | undefined
476
494
  };
@@ -1,7 +1,7 @@
1
1
  import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine, Subscription, Root, ResolverFilterData, ID, Int } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
3
  import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
- import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity } from '@memberjunction/core-entities';
4
+ import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJAIAgentRequestEntity } from '@memberjunction/core-entities';
5
5
  import { AgentRunner } from '@memberjunction/ai-agents';
6
6
  import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
7
7
  import { AIEngine } from '@memberjunction/aiengine';
@@ -154,7 +154,8 @@ export class RunAIAgentResolver extends ResolverBase {
154
154
  errorMessage: result.agentRun?.ErrorMessage,
155
155
  finalStep: result.agentRun?.FinalStep,
156
156
  cancelled: result.agentRun?.Status === 'Cancelled',
157
- cancellationReason: result.agentRun?.CancellationReason
157
+ cancellationReason: result.agentRun?.CancellationReason,
158
+ feedbackRequestId: result.feedbackRequestId
158
159
  };
159
160
 
160
161
  // Safely extract agent run data using GetAll() for proper serialization
@@ -433,6 +434,21 @@ export class RunAIAgentResolver extends ResolverBase {
433
434
 
434
435
  const executionTime = Date.now() - startTime;
435
436
 
437
+ // Sync feedback request if this is a continuation run (user responded via conversation)
438
+ if (lastRunId && result.agentRun?.ID) {
439
+ await this.syncFeedbackRequestFromConversation(
440
+ lastRunId,
441
+ result.agentRun.ID,
442
+ userMessage,
443
+ currentUser
444
+ );
445
+ }
446
+
447
+ // Send notification if agent created a feedback request (Chat step)
448
+ if (result.feedbackRequestId) {
449
+ await this.sendFeedbackRequestNotification(result, currentUser, pubSub, userPayload);
450
+ }
451
+
436
452
  // Create notification if enabled and artifact was created successfully
437
453
  if (createNotification && result.success && artifactInfo && artifactInfo.artifactId && artifactInfo.versionId && artifactInfo.versionNumber) {
438
454
  await this.createCompletionNotification(
@@ -752,6 +768,112 @@ export class RunAIAgentResolver extends ResolverBase {
752
768
  }
753
769
  }
754
770
 
771
+ /**
772
+ * When a continuation run completes (lastRunId was provided), sync the corresponding
773
+ * AIAgentRequest by marking it as responded. This keeps the dashboard accurate when
774
+ * users respond to Chat steps via the conversation UI.
775
+ *
776
+ * Called server-side in the resolver so the conversation UI doesn't need any changes.
777
+ */
778
+ private async syncFeedbackRequestFromConversation(
779
+ lastRunId: string,
780
+ newRunId: string,
781
+ userMessage: string | undefined,
782
+ contextUser: UserInfo
783
+ ): Promise<void> {
784
+ try {
785
+ const rv = new RunView();
786
+ const result = await rv.RunView<MJAIAgentRequestEntity>({
787
+ EntityName: 'MJ: AI Agent Requests',
788
+ ExtraFilter: `OriginatingAgentRunID='${lastRunId}' AND Status='Requested'`,
789
+ MaxRows: 1,
790
+ ResultType: 'entity_object'
791
+ }, contextUser);
792
+
793
+ if (!result.Success || !result.Results || result.Results.length === 0) {
794
+ return; // No pending request for this run — normal for non-Chat continuations
795
+ }
796
+
797
+ const request = result.Results[0];
798
+ request.Status = 'Responded';
799
+ request.RespondedAt = new Date();
800
+ request.ResponseByUserID = contextUser.ID;
801
+ request.ResumingAgentRunID = newRunId;
802
+ if (userMessage) {
803
+ request.Response = userMessage;
804
+ }
805
+
806
+ const saved = await request.Save();
807
+ if (saved) {
808
+ LogStatus(`📋 Synced feedback request ${request.ID} → Responded (via conversation)`);
809
+ } else {
810
+ LogError(`Failed to save feedback request sync for ${request.ID}`);
811
+ }
812
+ } catch (error) {
813
+ // Don't let sync failure break the agent execution
814
+ LogError(`Error syncing feedback request: ${(error as Error).message}`);
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Sends a notification when an agent creates a feedback request (Chat step).
820
+ * Called after execution completes if the result contains a feedbackRequestId.
821
+ */
822
+ private async sendFeedbackRequestNotification(
823
+ result: ExecuteAgentResult,
824
+ contextUser: UserInfo,
825
+ pubSub: PubSubEngine,
826
+ userPayload: UserPayload
827
+ ): Promise<void> {
828
+ if (!result.feedbackRequestId) {
829
+ return;
830
+ }
831
+
832
+ try {
833
+ // Get agent name
834
+ await AIEngine.Instance.Config(false, contextUser);
835
+ const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, result.agentRun?.AgentID));
836
+ const agentName = agent?.Name || 'Agent';
837
+
838
+ // Truncate message for notification
839
+ const message = result.agentRun?.Message || 'Agent needs your input';
840
+ const truncatedMessage = message.length > 200 ? message.substring(0, 197) + '...' : message;
841
+
842
+ const notificationEngine = NotificationEngine.Instance;
843
+ await notificationEngine.Config(false, contextUser);
844
+ const notifResult = await notificationEngine.SendNotification({
845
+ userId: contextUser.ID,
846
+ typeNameOrId: 'Agent Feedback Request',
847
+ title: `${agentName} needs your input`,
848
+ message: truncatedMessage,
849
+ resourceConfiguration: {
850
+ type: 'agent-request',
851
+ requestId: result.feedbackRequestId
852
+ }
853
+ }, contextUser);
854
+
855
+ if (notifResult.success && notifResult.inAppNotificationId) {
856
+ LogStatus(`📬 Feedback request notification sent (ID: ${notifResult.inAppNotificationId})`);
857
+
858
+ // Publish real-time notification event
859
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
860
+ userPayload: JSON.stringify(userPayload),
861
+ message: JSON.stringify({
862
+ type: 'notification',
863
+ notificationId: notifResult.inAppNotificationId,
864
+ action: 'create',
865
+ title: `${agentName} needs your input`,
866
+ message: truncatedMessage
867
+ })
868
+ });
869
+ } else if (!notifResult.success) {
870
+ LogError(`Feedback request notification failed: ${notifResult.errors?.join(', ')}`);
871
+ }
872
+ } catch (error) {
873
+ LogError(`Error sending feedback request notification: ${(error as Error).message}`);
874
+ }
875
+ }
876
+
755
877
  /**
756
878
  * Optimized mutation that loads conversation history server-side.
757
879
  * This avoids sending large attachment data from client to server.
@@ -854,6 +976,183 @@ export class RunAIAgentResolver extends ResolverBase {
854
976
  }
855
977
  }
856
978
 
979
+ /**
980
+ * Respond to a pending AIAgentRequest from the dashboard or API.
981
+ * Updates the request record with the response and optionally spawns a
982
+ * new agent run to resume execution with the human's input.
983
+ */
984
+ @Mutation(() => AIAgentRunResult)
985
+ async RespondToAgentRequest(
986
+ @Arg('requestId') requestId: string,
987
+ @Arg('status') status: string,
988
+ @Ctx() { userPayload, providers, dataSource }: AppContext,
989
+ @Arg('response', { nullable: true }) response?: string,
990
+ @Arg('responseData', { nullable: true }) responseData?: string,
991
+ @Arg('resumeAgent', { nullable: true }) resumeAgent?: boolean
992
+ ): Promise<AIAgentRunResult> {
993
+ const startTime = Date.now();
994
+ try {
995
+ const currentUser = this.GetUserFromPayload(userPayload);
996
+ if (!currentUser) {
997
+ throw new Error('Unable to determine current user');
998
+ }
999
+
1000
+ const md = new Metadata();
1001
+ const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
1002
+ 'MJ: AI Agent Requests',
1003
+ currentUser
1004
+ );
1005
+ if (!(await request.Load(requestId))) {
1006
+ throw new Error(`Agent request ${requestId} not found`);
1007
+ }
1008
+
1009
+ if (request.Status !== 'Requested') {
1010
+ throw new Error(`Request ${requestId} is already ${request.Status}, cannot respond`);
1011
+ }
1012
+
1013
+ // Validate status
1014
+ const validStatuses = ['Approved', 'Rejected', 'Responded'];
1015
+ if (!validStatuses.includes(status)) {
1016
+ throw new Error(`Invalid status "${status}". Must be one of: ${validStatuses.join(', ')}`);
1017
+ }
1018
+
1019
+ // Update the request
1020
+ request.Status = status as 'Approved' | 'Rejected' | 'Responded';
1021
+ request.Response = response || null;
1022
+ request.ResponseData = responseData || null;
1023
+ request.RespondedAt = new Date();
1024
+ request.ResponseByUserID = currentUser.ID;
1025
+
1026
+ const saved = await request.Save();
1027
+ if (!saved) {
1028
+ throw new Error(`Failed to save response for request ${requestId}`);
1029
+ }
1030
+
1031
+ LogStatus(`📋 Agent request ${requestId} → ${status} by ${currentUser.Email || currentUser.ID}`);
1032
+
1033
+ const executionTime = Date.now() - startTime;
1034
+ return {
1035
+ success: true,
1036
+ executionTimeMs: executionTime,
1037
+ result: JSON.stringify({
1038
+ success: true,
1039
+ requestId: requestId,
1040
+ status: status,
1041
+ resumed: false
1042
+ })
1043
+ };
1044
+ } catch (error) {
1045
+ const executionTime = Date.now() - startTime;
1046
+ const errorMessage = (error as Error).message || 'Unknown error';
1047
+ LogError(`RespondToAgentRequest failed: ${errorMessage}`, undefined, error);
1048
+ return {
1049
+ success: false,
1050
+ errorMessage,
1051
+ executionTimeMs: executionTime,
1052
+ result: JSON.stringify({ success: false, errorMessage })
1053
+ };
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Reassign an agent request to a different user.
1059
+ * Updates RequestForUserID and sends a notification to the new assignee.
1060
+ */
1061
+ @Mutation(() => AIAgentRunResult)
1062
+ async ReassignAgentRequest(
1063
+ @Arg('requestId') requestId: string,
1064
+ @Arg('newUserID') newUserID: string,
1065
+ @Ctx() { userPayload }: AppContext,
1066
+ @Arg('note', { nullable: true }) note?: string
1067
+ ): Promise<AIAgentRunResult> {
1068
+ const startTime = Date.now();
1069
+ try {
1070
+ const currentUser = this.GetUserFromPayload(userPayload);
1071
+ if (!currentUser) {
1072
+ throw new Error('Unable to determine current user');
1073
+ }
1074
+
1075
+ const md = new Metadata();
1076
+ const request = await md.GetEntityObject<MJAIAgentRequestEntity>(
1077
+ 'MJ: AI Agent Requests',
1078
+ currentUser
1079
+ );
1080
+ if (!(await request.Load(requestId))) {
1081
+ throw new Error(`Agent request ${requestId} not found`);
1082
+ }
1083
+
1084
+ if (request.Status !== 'Requested') {
1085
+ throw new Error(`Request ${requestId} is ${request.Status} and cannot be reassigned`);
1086
+ }
1087
+
1088
+ const previousUserID = request.RequestForUserID;
1089
+ request.RequestForUserID = newUserID;
1090
+
1091
+ // Append reassignment note to Comments
1092
+ if (note || previousUserID) {
1093
+ const timestamp = new Date().toISOString();
1094
+ const reassignEntry = `[${timestamp} by ${currentUser.Email || currentUser.ID}] Reassigned from ${previousUserID || '(unassigned)'} to ${newUserID}${note ? ` — "${note}"` : ''}`;
1095
+ request.Comments = request.Comments
1096
+ ? `${request.Comments}\n${reassignEntry}`
1097
+ : reassignEntry;
1098
+ }
1099
+
1100
+ const saved = await request.Save();
1101
+ if (!saved) {
1102
+ throw new Error(`Failed to save reassignment for request ${requestId}`);
1103
+ }
1104
+
1105
+ // Send notification to new assignee
1106
+ try {
1107
+ await AIEngine.Instance.Config(false, currentUser);
1108
+ const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, request.AgentID));
1109
+ const agentName = agent?.Name || 'Agent';
1110
+ const truncatedRequest = request.Request.length > 200
1111
+ ? request.Request.substring(0, 197) + '...'
1112
+ : request.Request;
1113
+
1114
+ const notificationEngine = NotificationEngine.Instance;
1115
+ await notificationEngine.Config(false, currentUser);
1116
+ await notificationEngine.SendNotification({
1117
+ userId: newUserID,
1118
+ typeNameOrId: 'Agent Feedback Request',
1119
+ title: `${agentName} request assigned to you`,
1120
+ message: truncatedRequest,
1121
+ resourceConfiguration: {
1122
+ type: 'agent-request',
1123
+ requestId: requestId
1124
+ }
1125
+ }, currentUser);
1126
+ } catch (notifError) {
1127
+ LogError(`Failed to send reassignment notification: ${(notifError as Error).message}`);
1128
+ }
1129
+
1130
+ LogStatus(`📋 Agent request ${requestId} reassigned to ${newUserID}`);
1131
+
1132
+ const executionTime = Date.now() - startTime;
1133
+ return {
1134
+ success: true,
1135
+ executionTimeMs: executionTime,
1136
+ result: JSON.stringify({
1137
+ success: true,
1138
+ requestId,
1139
+ newUserID,
1140
+ reassigned: true
1141
+ })
1142
+ };
1143
+ } catch (error) {
1144
+ const executionTime = Date.now() - startTime;
1145
+ const errorMessage = (error as Error).message || 'Unknown error';
1146
+ LogError(`ReassignAgentRequest failed: ${errorMessage}`, undefined, error);
1147
+ return {
1148
+ success: false,
1149
+ errorMessage,
1150
+ executionTimeMs: executionTime,
1151
+ result: JSON.stringify({ success: false, errorMessage })
1152
+ };
1153
+ }
1154
+ }
1155
+
857
1156
  /**
858
1157
  * Execute agent in background (fire-and-forget).
859
1158
  * Handles errors by publishing error completion events via PubSub,