@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.3
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.
- package/LICENSE +180 -4
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +65 -18
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +1 -1
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +540 -1595
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -16857
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +58 -21
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -54
- package/dist/index.js.map +1 -1
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +9 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +39 -0
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +5 -0
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -0
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -1
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +85 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +130 -44
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +37 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +34 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphActionRunner.js +95 -0
- package/dist/services/TaskGraphActionRunner.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +53 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
- package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphPromptRunner.js +134 -0
- package/dist/services/TaskGraphPromptRunner.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +1 -1
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
- package/dist/telephony/VonageTelephonyRouter.js +1 -1
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
- package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
- package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +1 -1
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +69 -21
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/config.ts +55 -36
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +2038 -10529
- package/src/generic/ResolverBase.ts +63 -21
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +199 -56
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/realtimeWidget/WidgetSessionService.ts +1 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
- package/src/resolvers/ActionResolver.ts +13 -0
- package/src/resolvers/ComponentRegistryResolver.ts +1 -1
- package/src/resolvers/EntityPermissionResolver.ts +1 -1
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/SearchEntitiesResolver.ts +1 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +150 -42
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +38 -1
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +58 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Worker-mode poll loop for durable integration sync runs (PR 1 item 8).
|
|
3
|
+
* @module MJServer/services
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
|
|
7
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
8
|
+
import { IntegrationEngine } from '@memberjunction/integration-engine';
|
|
9
|
+
import { IntegrationSyncWorkerConfig } from '../config.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Polls `CompanyIntegrationRun` for `Status='Queued'` rows, claims each one atomically,
|
|
13
|
+
* executes it, and releases — the enqueue/execute split that lets a sync outlive the
|
|
14
|
+
* request that asked for it.
|
|
15
|
+
*
|
|
16
|
+
* Multiple workers (in this process or others) may poll the same queue: the claim sproc
|
|
17
|
+
* admits exactly one, and every loser simply moves on to the next candidate. Nothing here
|
|
18
|
+
* assumes it is the only worker.
|
|
19
|
+
*/
|
|
20
|
+
export class IntegrationSyncWorkerService {
|
|
21
|
+
private config: IntegrationSyncWorkerConfig;
|
|
22
|
+
private systemUser: UserInfo | null = null;
|
|
23
|
+
private timer: NodeJS.Timeout | null = null;
|
|
24
|
+
private isRunning = false;
|
|
25
|
+
private polling = false;
|
|
26
|
+
/** Run IDs currently executing in THIS process — bounds concurrency and prevents self-collision. */
|
|
27
|
+
private inFlight = new Set<string>();
|
|
28
|
+
|
|
29
|
+
constructor(config: IntegrationSyncWorkerConfig) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public get IsEnabled(): boolean {
|
|
34
|
+
return this.config.enabled;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public get IsRunning(): boolean {
|
|
38
|
+
return this.isRunning;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Number of queued runs this worker is currently executing. */
|
|
42
|
+
public get InFlightCount(): number {
|
|
43
|
+
return this.inFlight.size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Resolve the system user the worker executes syncs as. */
|
|
47
|
+
public async Initialize(): Promise<void> {
|
|
48
|
+
if (!this.config.enabled) return;
|
|
49
|
+
|
|
50
|
+
const email = this.config.systemUserEmail;
|
|
51
|
+
this.systemUser = UserCache.Users.find(u => u.Email?.toLowerCase() === email.toLowerCase()) ?? null;
|
|
52
|
+
if (!this.systemUser) {
|
|
53
|
+
throw new Error(`[IntegrationSyncWorker] System user not found with email: ${email}`);
|
|
54
|
+
}
|
|
55
|
+
await IntegrationEngine.Instance.Config(false, this.systemUser);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Begin polling the queue. */
|
|
59
|
+
public Start(): void {
|
|
60
|
+
if (!this.config.enabled || this.isRunning) return;
|
|
61
|
+
if (!this.systemUser) {
|
|
62
|
+
throw new Error('[IntegrationSyncWorker] Not initialized — call Initialize() first');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.isRunning = true;
|
|
66
|
+
this.timer = setInterval(() => void this.pollOnce(), this.config.pollingIntervalMs);
|
|
67
|
+
// Standard level, NOT verboseOnly: this states a deployment-shaping fact — that this process
|
|
68
|
+
// executes queued syncs — and it's one line per process lifetime. Stop() already logs at
|
|
69
|
+
// standard level, so hiding Start() produced logs showing a worker stopping that never
|
|
70
|
+
// started, and no way to confirm from logs that a deployment is in worker mode at all.
|
|
71
|
+
LogStatus(
|
|
72
|
+
`🔄 Integration Sync Worker: polling every ${Math.round(this.config.pollingIntervalMs / 1000)}s, ` +
|
|
73
|
+
`up to ${this.config.maxConcurrentRuns} concurrent run(s)`
|
|
74
|
+
);
|
|
75
|
+
// Don't wait a full interval to drain a queue that already has work.
|
|
76
|
+
void this.pollOnce();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stop polling. In-flight runs are left to finish on their own — their leases are
|
|
81
|
+
* renewed by the engine's heartbeat, so killing them here would only strand rows.
|
|
82
|
+
*/
|
|
83
|
+
public Stop(): void {
|
|
84
|
+
if (!this.isRunning) return;
|
|
85
|
+
if (this.timer) {
|
|
86
|
+
clearInterval(this.timer);
|
|
87
|
+
this.timer = null;
|
|
88
|
+
}
|
|
89
|
+
this.isRunning = false;
|
|
90
|
+
LogStatus(`[IntegrationSyncWorker] Stopped polling (${this.inFlight.size} run(s) still in flight)`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One poll pass: fetch claimable candidates up to the free concurrency slots and start
|
|
95
|
+
* each. Overlapping passes are suppressed so a slow query can't stack up timers.
|
|
96
|
+
*/
|
|
97
|
+
private async pollOnce(): Promise<void> {
|
|
98
|
+
if (this.polling || !this.isRunning || !this.systemUser) return;
|
|
99
|
+
|
|
100
|
+
const slots = this.config.maxConcurrentRuns - this.inFlight.size;
|
|
101
|
+
if (slots <= 0) return;
|
|
102
|
+
|
|
103
|
+
this.polling = true;
|
|
104
|
+
try {
|
|
105
|
+
const candidates = await IntegrationEngine.PollQueuedRuns(this.systemUser, slots);
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
if (this.inFlight.has(candidate.ID)) continue;
|
|
108
|
+
this.startRun(candidate.ID);
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
LogError(`[IntegrationSyncWorker] Poll failed`, undefined, err);
|
|
112
|
+
} finally {
|
|
113
|
+
this.polling = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Execute one queued run in the background. Losing the claim race is normal — the run
|
|
119
|
+
* is simply someone else's — so it is logged as information, never as an error.
|
|
120
|
+
*/
|
|
121
|
+
private startRun(runID: string): void {
|
|
122
|
+
if (!this.systemUser) return;
|
|
123
|
+
|
|
124
|
+
this.inFlight.add(runID);
|
|
125
|
+
void IntegrationEngine.Instance
|
|
126
|
+
.ExecuteQueuedRun(runID, this.systemUser)
|
|
127
|
+
.then(result => {
|
|
128
|
+
if (result.Success) {
|
|
129
|
+
LogStatus(`[IntegrationSyncWorker] Run ${runID} completed: ${result.RecordsProcessed} record(s) processed`);
|
|
130
|
+
} else {
|
|
131
|
+
LogStatus(`[IntegrationSyncWorker] Run ${runID} did not complete: ${result.ErrorMessage ?? 'unknown reason'}`);
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.catch(err => {
|
|
135
|
+
LogError(`[IntegrationSyncWorker] Run ${runID} threw`, undefined, err);
|
|
136
|
+
})
|
|
137
|
+
.finally(() => {
|
|
138
|
+
this.inFlight.delete(runID);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { LogError, LogStatus, LogStatusEx, UserInfo } from '@memberjunction/core';
|
|
7
|
-
import { UserCache } from '@memberjunction/
|
|
7
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
8
8
|
import { SchedulingEngine } from '@memberjunction/scheduling-engine';
|
|
9
9
|
import { ScheduledJobsConfig } from '../config.js';
|
|
10
10
|
|
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { UserInfo } from '@memberjunction/core';
|
|
20
20
|
import { LoadTaskGraphOperations, TaskGraphDispatcher } from '@memberjunction/task-graph';
|
|
21
|
+
import { TaskGraphPromptRunner } from './TaskGraphPromptRunner.js';
|
|
21
22
|
import sql from 'mssql';
|
|
23
|
+
import { DurableEntityActionRegistry } from '@memberjunction/actions-base';
|
|
22
24
|
import { CreateTaskGraphProviderFactory } from './TaskGraphProviderFactory.js';
|
|
25
|
+
import { TaskGraphActionRunner } from './TaskGraphActionRunner.js';
|
|
23
26
|
import { TaskGraphAgentRunner } from './TaskGraphAgentRunner.js';
|
|
27
|
+
import { randomBytes } from 'node:crypto';
|
|
28
|
+
import { DurableEntityActionTaskSubmitter } from './DurableEntityActionTaskSubmitter.js';
|
|
24
29
|
import { TaskGraphContinuationDeliverer } from './TaskGraphContinuationDeliverer.js';
|
|
25
30
|
import { PubSubManager } from '../generic/PubSubManager.js';
|
|
26
31
|
import { TaskGraphFrameBroadcaster } from '../resolvers/TaskGraphFrameResolver.js';
|
|
@@ -35,6 +40,19 @@ import { TaskGraphFrameBroadcaster } from '../resolvers/TaskGraphFrameResolver.j
|
|
|
35
40
|
*
|
|
36
41
|
* The dispatcher self-registers with `ShutdownRegistry`, so the caller does not wire teardown.
|
|
37
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* A per-process identity for the claim protocol.
|
|
45
|
+
*
|
|
46
|
+
* Host and pid are kept because they are what a human reads in a log; the random suffix is what
|
|
47
|
+
* makes the value actually unique. Generated once per call, so a restart is a new instance — which
|
|
48
|
+
* is correct: the previous process's claims should expire rather than be inherited.
|
|
49
|
+
*/
|
|
50
|
+
function defaultInstanceID(): string {
|
|
51
|
+
const host = process.env.HOSTNAME ?? 'mjapi';
|
|
52
|
+
const entropy = randomBytes(4).toString('hex');
|
|
53
|
+
return `${host}-${process.pid}-${entropy}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
export async function StartTaskGraphDispatcher(
|
|
39
57
|
pool: sql.ConnectionPool,
|
|
40
58
|
contextUser: UserInfo,
|
|
@@ -48,14 +66,33 @@ export async function StartTaskGraphDispatcher(
|
|
|
48
66
|
providerFactory,
|
|
49
67
|
new TaskGraphAgentRunner(),
|
|
50
68
|
contextUser,
|
|
51
|
-
|
|
69
|
+
// A RANDOM SUFFIX, because host+pid is not unique in the deployments this runs in (C2).
|
|
70
|
+
// `HOSTNAME` is not exported to child processes under systemd or pm2, and a containerised
|
|
71
|
+
// process is routinely pid 1 — so two hosts default to the same `mjapi-1`. A shared instance
|
|
72
|
+
// id defeats every `ClaimedBy=@me` guard at once: A's heartbeat renews B's lease, and A's
|
|
73
|
+
// stale terminal write lands over B's live execution. An explicit `instanceID` still wins,
|
|
74
|
+
// for hosts that have a real identity to supply.
|
|
75
|
+
{ InstanceID: instanceID ?? defaultInstanceID() },
|
|
52
76
|
// Without this the dispatcher had nowhere to deliver: a finished graph logged its outcome,
|
|
53
77
|
// marked itself delivered, and said nothing to the conversation that asked for it.
|
|
54
78
|
new TaskGraphContinuationDeliverer(providerFactory, contextUser),
|
|
55
79
|
// Live signal. Supplied only when a PubSub engine exists — in a worker or a test there is
|
|
56
80
|
// nobody to publish to, and the dispatcher runs identically without one.
|
|
57
81
|
pubSub ? new TaskGraphFrameBroadcaster(pubSub) : undefined,
|
|
82
|
+
// Action nodes — the third execution shape, beside agents and people. Durable After* entity
|
|
83
|
+
// actions land as these.
|
|
84
|
+
new TaskGraphActionRunner(),
|
|
85
|
+
// Prompt nodes. Without this a Flow agent with a Prompt step submits and then
|
|
86
|
+
// stalls on a task nothing can execute — which is why the shipped User Onboarding
|
|
87
|
+
// Flow Agent could not run at all before this seam existed.
|
|
88
|
+
new TaskGraphPromptRunner(),
|
|
58
89
|
);
|
|
59
90
|
await dispatcher.Start();
|
|
91
|
+
|
|
92
|
+
// Registered only AFTER the dispatcher is running, and only here. A submitter without a
|
|
93
|
+
// dispatcher writes Task rows nobody picks up — strictly worse than staying inline, because the
|
|
94
|
+
// work is now both undone and invisible in the place it used to be logged.
|
|
95
|
+
DurableEntityActionRegistry.Instance.Register(new DurableEntityActionTaskSubmitter(providerFactory));
|
|
96
|
+
|
|
60
97
|
return dispatcher;
|
|
61
98
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MJServer's implementation of the task-graph `TaskActionRunner` seam.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to {@link TaskGraphAgentRunner}: the dispatcher knows *when* an action-assigned
|
|
5
|
+
* node should run and nothing about how an action executes, so execution is injected. Keeping it
|
|
6
|
+
* here rather than in `@memberjunction/task-graph` is not tidiness — the action engine depends on
|
|
7
|
+
* the entity layer the dispatcher also builds on, and importing it there would make every consumer
|
|
8
|
+
* of durable execution load the action engine as well.
|
|
9
|
+
*
|
|
10
|
+
* @module @memberjunction/server
|
|
11
|
+
*/
|
|
12
|
+
import { ActionEngineServer } from '@memberjunction/actions';
|
|
13
|
+
import { ActionParam } from '@memberjunction/actions-base';
|
|
14
|
+
import { LogError } from '@memberjunction/core';
|
|
15
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
16
|
+
import type { TaskActionRunner, TaskActionRunParams, TaskActionRunResult } from '@memberjunction/task-graph';
|
|
17
|
+
|
|
18
|
+
export class TaskGraphActionRunner implements TaskActionRunner {
|
|
19
|
+
public async RunActionForTask(params: TaskActionRunParams): Promise<TaskActionRunResult> {
|
|
20
|
+
try {
|
|
21
|
+
await ActionEngineServer.Instance.Config(false, params.ContextUser);
|
|
22
|
+
const action = ActionEngineServer.Instance.Actions.find((a) => UUIDsEqual(a.ID, params.ActionID));
|
|
23
|
+
if (!action) {
|
|
24
|
+
return { Success: false, ErrorMessage: `Action ${params.ActionID} is not in the engine's metadata.` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await ActionEngineServer.Instance.RunAction({
|
|
28
|
+
Action: action,
|
|
29
|
+
ContextUser: params.ContextUser,
|
|
30
|
+
Params: this.buildParams(params),
|
|
31
|
+
// No Filters. A durable node is work that a filter ALREADY let through — the entity
|
|
32
|
+
// action's gate ran at dispatch time, on the change context that no longer exists by
|
|
33
|
+
// the time the dispatcher picks the task up. Re-evaluating here would ask a
|
|
34
|
+
// transition question with nothing to answer it, and fail closed on every retry.
|
|
35
|
+
Filters: [],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
Success: result.Success,
|
|
40
|
+
Output: this.buildOutput(result.Params),
|
|
41
|
+
ErrorMessage: result.Success ? undefined : result.Message,
|
|
42
|
+
// The engine already wrote the log; this is the only place its id is in hand. Without
|
|
43
|
+
// carrying it out, a workflow's action step has no path back to its own execution
|
|
44
|
+
// record — the one thing anyone wants when an action misbehaves.
|
|
45
|
+
ActionLogID: result.LogEntry?.ID,
|
|
46
|
+
};
|
|
47
|
+
} catch (e) {
|
|
48
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
49
|
+
LogError(`[TaskGraphActionRunner] Task ${params.TaskID} failed: ${message}`);
|
|
50
|
+
return { Success: false, ErrorMessage: message };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Projects the action's parameters onto the flat result a workflow step is expected to produce.
|
|
56
|
+
*
|
|
57
|
+
* **Why not return `result.Params` as-is.** That is an `ActionParam[]`, and every consumer of a
|
|
58
|
+
* step's output addresses fields BY NAME: an output mapping says `{"CurrentPrice": "stockPrice"}`,
|
|
59
|
+
* and a branch condition then reads `payload.stockPrice`. Handed an array, the mapping finds no
|
|
60
|
+
* `CurrentPrice`, writes nothing, and the condition evaluates against `undefined` — which is
|
|
61
|
+
* falsy rather than erroneous, so the workflow takes the other branch and reports success. That
|
|
62
|
+
* is exactly what the Demo workflow did: step 1 completed with an output payload of `{}` and
|
|
63
|
+
* every downstream branch was skipped, with nothing anywhere reporting a problem.
|
|
64
|
+
*
|
|
65
|
+
* Only `Output` and `Both` params are included. Echoing the inputs back would let a step's own
|
|
66
|
+
* parameters masquerade as its results, so an output mapping could "succeed" by reading the
|
|
67
|
+
* value the step was given rather than the one it produced.
|
|
68
|
+
*/
|
|
69
|
+
private buildOutput(actionParams: ActionParam[]): Record<string, unknown> {
|
|
70
|
+
const output: Record<string, unknown> = {};
|
|
71
|
+
for (const p of actionParams ?? []) {
|
|
72
|
+
if (p.Type === 'Output' || p.Type === 'Both') output[p.Name] = p.Value;
|
|
73
|
+
}
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Rebuilds the action's parameters from the task's stored payload.
|
|
79
|
+
*
|
|
80
|
+
* **The values here have been through redaction**, because `Task.InputPayload` is persistent,
|
|
81
|
+
* user-visible storage and nothing writes a raw `ActionParam[]` there. A parameter the binding
|
|
82
|
+
* marked as not-logged therefore arrives absent rather than secret — the action sees a missing
|
|
83
|
+
* value, which is the honest consequence of choosing not to persist it, and the reason durable
|
|
84
|
+
* dispatch is opt-in per binding rather than the default.
|
|
85
|
+
*
|
|
86
|
+
* Dependency outputs are merged underneath the task's own input so a node's explicit parameters
|
|
87
|
+
* always win over an upstream node that happened to emit the same key.
|
|
88
|
+
*/
|
|
89
|
+
private buildParams(params: TaskActionRunParams): ActionParam[] {
|
|
90
|
+
const merged: Record<string, unknown> = {};
|
|
91
|
+
for (const [name, value] of params.DependencyOutputs) {
|
|
92
|
+
merged[name] = value;
|
|
93
|
+
}
|
|
94
|
+
if (params.InputPayload && typeof params.InputPayload === 'object') {
|
|
95
|
+
Object.assign(merged, params.InputPayload as Record<string, unknown>);
|
|
96
|
+
}
|
|
97
|
+
return Object.entries(merged).map(([Name, Value]) => ({ Name, Value, Type: 'Input' }));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
import { AgentRunner } from '@memberjunction/ai-agents';
|
|
16
16
|
import { ChatMessageRole } from '@memberjunction/ai';
|
|
17
17
|
import { LogError } from '@memberjunction/core';
|
|
18
|
-
import { MJAIAgentEntityExtended } from '@memberjunction/ai-core-plus';
|
|
18
|
+
import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended } from '@memberjunction/ai-core-plus';
|
|
19
|
+
import type { AgentExecutionProgressCallback } from '@memberjunction/ai-core-plus';
|
|
19
20
|
import type { TaskAgentRunner, TaskAgentRunParams, TaskAgentRunResult } from '@memberjunction/task-graph';
|
|
20
21
|
|
|
21
22
|
export class TaskGraphAgentRunner implements TaskAgentRunner {
|
|
@@ -30,6 +31,14 @@ export class TaskGraphAgentRunner implements TaskAgentRunner {
|
|
|
30
31
|
agent,
|
|
31
32
|
conversationMessages: [{ role: ChatMessageRole.user, content: this.buildPrompt(params) }],
|
|
32
33
|
contextUser: params.ContextUser,
|
|
34
|
+
// Inherited from the graph, so a graph this run goes on to emit is one hop deeper
|
|
35
|
+
// rather than starting the chain over. Without it MAX_REINVOKE_DEPTH never fires.
|
|
36
|
+
continuationDepth: params.ContinuationDepth ?? 0,
|
|
37
|
+
parentRun: await this.loadSubmittingRun(params),
|
|
38
|
+
// The bridge from the agent's own progress stream up to graph-level observers: the
|
|
39
|
+
// dispatcher turns these into rate-limited `NodeProgress` frames, which is how a
|
|
40
|
+
// running Agent step shows "what it is doing right now" on the workflow canvas.
|
|
41
|
+
onProgress: this.bridgeProgress(params),
|
|
33
42
|
});
|
|
34
43
|
|
|
35
44
|
const success = result?.success === true;
|
|
@@ -46,6 +55,54 @@ export class TaskGraphAgentRunner implements TaskAgentRunner {
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Adapts the agent framework's progress callback onto the dispatcher's plain sink.
|
|
60
|
+
*
|
|
61
|
+
* Undefined when nobody is listening, so the agent run pays nothing for observability it is not
|
|
62
|
+
* providing. Never throws — a progress announcement must not be able to fail the step.
|
|
63
|
+
*/
|
|
64
|
+
private bridgeProgress(params: TaskAgentRunParams): AgentExecutionProgressCallback | undefined {
|
|
65
|
+
const sink = params.OnProgress;
|
|
66
|
+
if (!sink) return undefined;
|
|
67
|
+
return (progress) => {
|
|
68
|
+
try {
|
|
69
|
+
sink(progress.message, progress.percentage);
|
|
70
|
+
} catch {
|
|
71
|
+
// The dispatcher's sink already guards itself; this is belt over braces.
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The run that submitted this graph, as the entity `RunAgent` wants for `parentRun`.
|
|
78
|
+
*
|
|
79
|
+
* **Why this link is worth a load.** It sets `ParentRunID` on the spawned run, which turns a
|
|
80
|
+
* workflow's total cost into one indexed sum over `RootParentRunID` rather than a walk of the
|
|
81
|
+
* graph, and gives the run a "started by" trail someone can follow back. Without it every
|
|
82
|
+
* dispatched run looks like it began from nowhere.
|
|
83
|
+
*
|
|
84
|
+
* Undefined — never a throw — when there is no submitting run or it cannot be read. A graph
|
|
85
|
+
* submitted by a schedule, by MCP, or by a person genuinely has no parent run, and failing the
|
|
86
|
+
* step because its provenance is unavailable would trade real work for a bookkeeping detail.
|
|
87
|
+
*/
|
|
88
|
+
private async loadSubmittingRun(params: TaskAgentRunParams): Promise<MJAIAgentRunEntityExtended | undefined> {
|
|
89
|
+
if (!params.SubmittingAgentRunID) return undefined;
|
|
90
|
+
try {
|
|
91
|
+
const run = await params.Provider.GetEntityObject<MJAIAgentRunEntityExtended>(
|
|
92
|
+
'MJ: AI Agent Runs',
|
|
93
|
+
params.ContextUser,
|
|
94
|
+
);
|
|
95
|
+
return (await run.Load(params.SubmittingAgentRunID)) ? run : undefined;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
LogError(
|
|
98
|
+
`[TaskGraphAgentRunner] Task ${params.TaskID}: submitting run ` +
|
|
99
|
+
`${params.SubmittingAgentRunID} could not be loaded; the spawned run will have no ` +
|
|
100
|
+
`parent link. ${e instanceof Error ? e.message : String(e)}`,
|
|
101
|
+
);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
49
106
|
/**
|
|
50
107
|
* Builds the agent's instruction from the task's own input plus its dependencies' outputs.
|
|
51
108
|
*
|
|
@@ -135,7 +135,17 @@ export class TaskGraphContinuationDeliverer implements TaskContinuationDeliverer
|
|
|
135
135
|
continuationDepth: params.ReinvokeDepth + 1,
|
|
136
136
|
});
|
|
137
137
|
} catch (e) {
|
|
138
|
-
|
|
138
|
+
// FALL BACK, don't just log (C3). Both load-failure paths above already post instead of
|
|
139
|
+
// reinvoking, on the principle that a completed workflow's outcome must reach the user
|
|
140
|
+
// somehow — but a throw out of `RunAgent` skipped that and lost the outcome entirely.
|
|
141
|
+
// The marker has been claimed by the time this runs, so nothing will look at the graph
|
|
142
|
+
// again: the work finished, and the only record of it is a server log line.
|
|
143
|
+
LogError(`[TaskGraphContinuationDeliverer] Reinvoking for "${params.WorkflowName}" threw — posting instead`, undefined, e);
|
|
144
|
+
try {
|
|
145
|
+
await this.PostMessage(params);
|
|
146
|
+
} catch (fallbackError) {
|
|
147
|
+
LogError(`[TaskGraphContinuationDeliverer] The fallback post for "${params.WorkflowName}" also failed`, undefined, fallbackError);
|
|
148
|
+
}
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MJServer's implementation of the task-graph `TaskPromptRunner` seam.
|
|
3
|
+
*
|
|
4
|
+
* The third sibling of {@link TaskGraphAgentRunner} and {@link TaskGraphActionRunner}, and here for
|
|
5
|
+
* the same reason: the dispatcher knows *when* a prompt-assigned node should run and nothing about
|
|
6
|
+
* how a prompt executes. The AI engine depends on the entity layer the dispatcher also builds on, so
|
|
7
|
+
* importing it into `@memberjunction/task-graph` would make every consumer of durable execution load
|
|
8
|
+
* the prompt engine too.
|
|
9
|
+
*
|
|
10
|
+
* **Why prompt nodes matter enough to have their own runner.** The `User Onboarding Flow Agent`
|
|
11
|
+
* ships with six of them. Until this existed, every one of its steps was refused at submission and
|
|
12
|
+
* the agent could not run at all — the refusal was correct (a persisted step nothing can execute
|
|
13
|
+
* waits forever) but it made a shipped workflow unusable rather than merely unfinished.
|
|
14
|
+
*
|
|
15
|
+
* @module @memberjunction/server
|
|
16
|
+
*/
|
|
17
|
+
import { AIPromptRunner } from '@memberjunction/ai-prompts';
|
|
18
|
+
import { AIEngine } from '@memberjunction/aiengine';
|
|
19
|
+
import { AIPromptParams } from '@memberjunction/ai-core-plus';
|
|
20
|
+
import { LogError } from '@memberjunction/core';
|
|
21
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
22
|
+
import type { TaskPromptRunner, TaskPromptRunParams, TaskPromptRunResult } from '@memberjunction/task-graph';
|
|
23
|
+
|
|
24
|
+
/** The placeholder a workflow prompt uses to receive the payload (plan §5.7). */
|
|
25
|
+
const CURRENT_PAYLOAD_PLACEHOLDER = '_CURRENT_PAYLOAD';
|
|
26
|
+
|
|
27
|
+
export class TaskGraphPromptRunner implements TaskPromptRunner {
|
|
28
|
+
public async RunPromptForTask(params: TaskPromptRunParams): Promise<TaskPromptRunResult> {
|
|
29
|
+
try {
|
|
30
|
+
await AIEngine.Instance.Config(false, params.ContextUser);
|
|
31
|
+
const prompt = AIEngine.Instance.Prompts.find((p) => UUIDsEqual(p.ID, params.PromptID));
|
|
32
|
+
if (!prompt) {
|
|
33
|
+
return { Success: false, ErrorMessage: `Prompt ${params.PromptID} is not in the engine's metadata.` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const promptParams = new AIPromptParams();
|
|
37
|
+
promptParams.prompt = prompt;
|
|
38
|
+
promptParams.contextUser = params.ContextUser;
|
|
39
|
+
promptParams.data = this.buildData(params);
|
|
40
|
+
|
|
41
|
+
const result = await new AIPromptRunner().ExecutePrompt(promptParams);
|
|
42
|
+
if (!result?.success) {
|
|
43
|
+
return {
|
|
44
|
+
Success: false,
|
|
45
|
+
ErrorMessage: result?.errorMessage ?? 'The prompt did not return a result.',
|
|
46
|
+
PromptRunID: result?.promptRun?.ID,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parsed = this.parseResponse(result.result);
|
|
51
|
+
return {
|
|
52
|
+
Success: true,
|
|
53
|
+
Output: parsed.payload,
|
|
54
|
+
PromptRunID: result.promptRun?.ID,
|
|
55
|
+
ChatMessage: parsed.chatMessage,
|
|
56
|
+
};
|
|
57
|
+
} catch (e) {
|
|
58
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
59
|
+
LogError(`[TaskGraphPromptRunner] Task ${params.TaskID} failed: ${message}`);
|
|
60
|
+
return { Success: false, ErrorMessage: message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What the prompt's template can see.
|
|
66
|
+
*
|
|
67
|
+
* The payload arrives under a **placeholder name** rather than being spread across the top level,
|
|
68
|
+
* so a workflow author writes `{{ _CURRENT_PAYLOAD }}` and gets the whole thing — and a payload
|
|
69
|
+
* key can never collide with a template parameter and silently win.
|
|
70
|
+
*
|
|
71
|
+
* `flowContext` carries what the step knows about its own position: what its prerequisites
|
|
72
|
+
* produced. Without it a prompt is reasoning about the payload alone and cannot refer to the step
|
|
73
|
+
* immediately before it.
|
|
74
|
+
*/
|
|
75
|
+
private buildData(params: TaskPromptRunParams): Record<string, unknown> {
|
|
76
|
+
const payload = params.InputPayload && typeof params.InputPayload === 'object'
|
|
77
|
+
? params.InputPayload as Record<string, unknown>
|
|
78
|
+
: {};
|
|
79
|
+
|
|
80
|
+
const dependencyOutputs: Record<string, unknown> = {};
|
|
81
|
+
for (const [taskID, output] of params.DependencyOutputs) dependencyOutputs[taskID] = output;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...(params.TemplateParameters ?? {}),
|
|
85
|
+
// JSON, not the object. A template writes `{{ _CURRENT_PAYLOAD }}` and expects to SEE
|
|
86
|
+
// the payload; handing the renderer an object yields "[object Object]", and the model
|
|
87
|
+
// then answers about a value it was never shown. That failure is invisible from the
|
|
88
|
+
// outside — the prompt run succeeds, the response is well-formed, and it is about
|
|
89
|
+
// nothing. It is what kept the Content Pipeline reviewer rejecting a draft it could not
|
|
90
|
+
// read, on every iteration, forever.
|
|
91
|
+
[CURRENT_PAYLOAD_PLACEHOLDER]: JSON.stringify(payload, null, 2),
|
|
92
|
+
flowContext: JSON.stringify({ dependencyOutputs }, null, 2),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reads the model's response as a payload update, and notices when it wants to stop.
|
|
98
|
+
*
|
|
99
|
+
* **A prompt is the only node kind that can end the workflow early**, because it is the only one
|
|
100
|
+
* doing open-ended reasoning: it can conclude the remaining steps are unnecessary. That is
|
|
101
|
+
* expressed as a Chat-shaped response, and the dispatcher honours it by skipping what remains and
|
|
102
|
+
* settling the parent Complete — rather than treating an early finish as an abandoned graph.
|
|
103
|
+
*
|
|
104
|
+
* Non-JSON is not an error. A prompt that answers in prose has still answered; it simply carries
|
|
105
|
+
* no structured update, so it becomes a message rather than a payload merge.
|
|
106
|
+
*/
|
|
107
|
+
private parseResponse(raw: unknown): { payload?: Record<string, unknown>; chatMessage?: string } {
|
|
108
|
+
if (raw == null) return {};
|
|
109
|
+
|
|
110
|
+
if (typeof raw === 'object') {
|
|
111
|
+
return this.interpret(raw as Record<string, unknown>);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof raw === 'string') {
|
|
115
|
+
const text = raw.trim();
|
|
116
|
+
const fenced = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
|
117
|
+
try {
|
|
118
|
+
const parsed: unknown = JSON.parse(fenced);
|
|
119
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
120
|
+
return this.interpret(parsed as Record<string, unknown>);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Prose, not JSON — see above.
|
|
124
|
+
}
|
|
125
|
+
return { chatMessage: text };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Separates "here is an update" from "we are done, tell them this". */
|
|
132
|
+
private interpret(obj: Record<string, unknown>): { payload?: Record<string, unknown>; chatMessage?: string } {
|
|
133
|
+
const nextStep = obj['nextStep'] as { type?: string } | undefined;
|
|
134
|
+
const wantsChat = nextStep?.type === 'Chat' || obj['taskComplete'] === true;
|
|
135
|
+
const message = typeof obj['message'] === 'string' ? obj['message'] : undefined;
|
|
136
|
+
|
|
137
|
+
if (wantsChat && message) {
|
|
138
|
+
// The payload still merges: a prompt that both concluded and produced data should not
|
|
139
|
+
// lose the data because it also had something to say.
|
|
140
|
+
return { payload: obj, chatMessage: message };
|
|
141
|
+
}
|
|
142
|
+
return { payload: obj };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { RunView, UserInfo, IMetadataProvider, Metadata, LogError, LogStatus } from '@memberjunction/core';
|
|
21
21
|
import type { MJAIBridgeAgentIdentityEntity, MJAIBridgeProviderEntity } from '@memberjunction/core-entities';
|
|
22
|
-
import { UserCache } from '@memberjunction/
|
|
22
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
23
23
|
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
|
|
24
24
|
import { CreateBridgeRealtimeSession } from '@memberjunction/ai-agents';
|
|
25
25
|
import {
|
|
@@ -20,7 +20,7 @@ import { Router, urlencoded, type Request, type Response } from 'express';
|
|
|
20
20
|
import { WebSocketServer, type WebSocket } from 'ws';
|
|
21
21
|
import { RegisterMediaUpgradeRoute } from './media-upgrade-router.js';
|
|
22
22
|
import { LogError, LogStatus, UserInfo, IMetadataProvider, Metadata } from '@memberjunction/core';
|
|
23
|
-
import { UserCache } from '@memberjunction/
|
|
23
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
24
24
|
import {
|
|
25
25
|
verifyTwilioSignature,
|
|
26
26
|
resolveInboundCall,
|
|
@@ -26,7 +26,7 @@ import { WebSocketServer, type WebSocket } from 'ws';
|
|
|
26
26
|
import type { IncomingMessage } from 'node:http';
|
|
27
27
|
import { RegisterMediaUpgradeRoute } from './media-upgrade-router.js';
|
|
28
28
|
import { LogError, LogStatus, UserInfo, IMetadataProvider, Metadata } from '@memberjunction/core';
|
|
29
|
-
import { UserCache } from '@memberjunction/
|
|
29
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
30
30
|
import {
|
|
31
31
|
verifyVonageSignature,
|
|
32
32
|
verifyVonageJwt,
|
package/src/types.ts
CHANGED
|
@@ -107,6 +107,12 @@ export type RunViewGenericParams = {
|
|
|
107
107
|
* happens. Propagated to `RunViewParams.BypassCache`.
|
|
108
108
|
*/
|
|
109
109
|
bypassCache?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Optional source-of-truth selector for entities that have a base-view materialization.
|
|
112
|
+
* 'Materialized' routes the read to the entity's materialized wrapper view; defaults to 'Live'.
|
|
113
|
+
* Propagated to `RunViewParams.DataSource`.
|
|
114
|
+
*/
|
|
115
|
+
dataSource?: 'Live' | 'Materialized';
|
|
110
116
|
};
|
|
111
117
|
|
|
112
118
|
|