@memberjunction/server 6.1.0-edge.0 → 6.1.0-edge.2
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/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +9 -5
- package/dist/auth/index.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/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +208 -646
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1123 -3773
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +19 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +51 -17
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -42
- package/dist/index.js.map +1 -1
- 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/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/IntegrationDiscoveryResolver.d.ts +9 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +3 -1
- 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/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/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 +85 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -0
- package/dist/resolvers/TaskGraphFrameResolver.js +163 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -0
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +1 -1
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts +0 -4
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +1 -15
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- 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/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +33 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.js +46 -0
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -0
- 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 +28 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphAgentRunner.js +104 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +70 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.js +162 -0
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -0
- 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/services/TaskGraphProviderFactory.d.ts +39 -0
- package/dist/services/TaskGraphProviderFactory.d.ts.map +1 -0
- package/dist/services/TaskGraphProviderFactory.js +28 -0
- package/dist/services/TaskGraphProviderFactory.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/package.json +92 -89
- 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__/SessionManager.test.ts +1 -1
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +249 -0
- package/src/__tests__/TaskGraphFrameResolver.test.ts +136 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +10 -6
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +955 -2735
- package/src/generic/ResolverBase.ts +52 -17
- package/src/index.ts +108 -43
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -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/IntegrationDiscoveryResolver.ts +12 -2
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- 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 +160 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/rest/OAuthCallbackHandler.ts +1 -1
- package/src/rest/RESTEndpointHandler.ts +1 -15
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +78 -0
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +115 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +181 -0
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/services/TaskGraphProviderFactory.ts +53 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/dist/resolvers/ReportResolver.d.ts +0 -24
- package/dist/resolvers/ReportResolver.d.ts.map +0 -1
- package/dist/resolvers/ReportResolver.js +0 -198
- package/dist/resolvers/ReportResolver.js.map +0 -1
- package/dist/resolvers/TaskResolver.d.ts +0 -31
- package/dist/resolvers/TaskResolver.d.ts.map +0 -1
- package/dist/resolvers/TaskResolver.js +0 -157
- package/dist/resolvers/TaskResolver.js.map +0 -1
- package/dist/services/TaskOrchestrator.d.ts +0 -130
- package/dist/services/TaskOrchestrator.d.ts.map +0 -1
- package/dist/services/TaskOrchestrator.js +0 -692
- package/dist/services/TaskOrchestrator.js.map +0 -1
- package/src/resolvers/ReportResolver.ts +0 -165
- package/src/resolvers/TaskResolver.ts +0 -148
- package/src/resolvers/__tests__/ReportResolver.test.ts +0 -279
- package/src/services/TaskOrchestrator.ts +0 -873
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Live signal for durable task graphs — the dispatcher's lifecycle frames, published
|
|
3
|
+
* to anyone watching a given graph.
|
|
4
|
+
*
|
|
5
|
+
* **Why frames rather than cache invalidation.** The alternative considered was emitting
|
|
6
|
+
* invalidation events after each CAS claim so clients re-read Task rows. That works, but it makes
|
|
7
|
+
* every consumer reconstruct meaning by diffing rows, and couples the UI to claim mechanics. A
|
|
8
|
+
* frame says what happened — "this step failed, here is why" — so a viewer renders progress
|
|
9
|
+
* directly. This is §3.4's originally-promised mechanism.
|
|
10
|
+
*
|
|
11
|
+
* **Addressed by `ParentTaskID`, deliberately not by session.** A durable graph outlives the tab
|
|
12
|
+
* that submitted it, and may be started by a schedule with no session at all. Keying on the graph
|
|
13
|
+
* means "watch this workflow run" works for whoever is permitted to see it, whenever they arrive —
|
|
14
|
+
* including after a page refresh, which a session-keyed push cannot survive.
|
|
15
|
+
*
|
|
16
|
+
* @module @memberjunction/server
|
|
17
|
+
*/
|
|
18
|
+
import { Arg, Field, ID, ObjectType, PubSubEngine, Resolver, ResolverFilterData, Root, Subscription } from 'type-graphql';
|
|
19
|
+
import { LogError } from '@memberjunction/core';
|
|
20
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
21
|
+
import type { TaskGraphFrame, TaskGraphObserver } from '@memberjunction/task-graph';
|
|
22
|
+
|
|
23
|
+
export const TASK_GRAPH_FRAMES_TOPIC = 'TASK_GRAPH_FRAMES';
|
|
24
|
+
|
|
25
|
+
/** One lifecycle event, as it reaches a subscriber. */
|
|
26
|
+
@ObjectType()
|
|
27
|
+
export class TaskGraphFrameNotification {
|
|
28
|
+
@Field(() => String)
|
|
29
|
+
kind!: string;
|
|
30
|
+
|
|
31
|
+
@Field(() => ID)
|
|
32
|
+
parentTaskId!: string;
|
|
33
|
+
|
|
34
|
+
@Field(() => ID, { nullable: true })
|
|
35
|
+
taskId?: string;
|
|
36
|
+
|
|
37
|
+
@Field(() => String, { nullable: true })
|
|
38
|
+
taskName?: string;
|
|
39
|
+
|
|
40
|
+
@Field(() => String, { nullable: true })
|
|
41
|
+
status?: string;
|
|
42
|
+
|
|
43
|
+
@Field(() => String, { nullable: true })
|
|
44
|
+
errorMessage?: string;
|
|
45
|
+
|
|
46
|
+
@Field(() => ID, { nullable: true })
|
|
47
|
+
assignedUserId?: string;
|
|
48
|
+
|
|
49
|
+
@Field(() => Number, { nullable: true })
|
|
50
|
+
completedCount?: number;
|
|
51
|
+
|
|
52
|
+
@Field(() => Number, { nullable: true })
|
|
53
|
+
totalCount?: number;
|
|
54
|
+
|
|
55
|
+
@Field(() => Date)
|
|
56
|
+
date!: Date;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wire payload. Identical to {@link TaskGraphFrame} plus the graph's owner, which exists ONLY as the
|
|
61
|
+
* server-side filter key and is never returned to a subscriber — the same discipline
|
|
62
|
+
* `PushStatusNotificationPayload` uses for `ownerUserId`.
|
|
63
|
+
*/
|
|
64
|
+
export interface TaskGraphFramePayload extends TaskGraphFrame {
|
|
65
|
+
/** User the graph belongs to. Filter key; never sent to the client. */
|
|
66
|
+
ownerUserId: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface TaskGraphFrameArgs {
|
|
70
|
+
parentTaskId: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Minimal shape of the subscription's connection context needed by the filter. */
|
|
74
|
+
export interface TaskGraphFrameFilterContext {
|
|
75
|
+
userPayload?: { userRecord?: { ID?: string } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delivery predicate. A frame reaches a subscriber only when BOTH hold:
|
|
80
|
+
* 1. it belongs to the graph the subscriber asked to watch, AND
|
|
81
|
+
* 2. the graph's owner matches the subscriber CONNECTION's authenticated identity.
|
|
82
|
+
*
|
|
83
|
+
* Condition 2 is load-bearing, and for the same reason `statusUpdatesFilter` needs it: a
|
|
84
|
+
* `parentTaskId` is discoverable, so without an identity check anyone holding one could watch
|
|
85
|
+
* another user's workflow — including its per-step error messages. **Fails closed** — a missing
|
|
86
|
+
* identity on either side never matches.
|
|
87
|
+
*/
|
|
88
|
+
export function taskGraphFrameFilter(data: {
|
|
89
|
+
payload: TaskGraphFramePayload;
|
|
90
|
+
args: TaskGraphFrameArgs;
|
|
91
|
+
context: TaskGraphFrameFilterContext | undefined;
|
|
92
|
+
}): boolean {
|
|
93
|
+
const { payload, args, context } = data;
|
|
94
|
+
if (!payload.ParentTaskID || !args.parentTaskId) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (!UUIDsEqual(payload.ParentTaskID, args.parentTaskId)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const connectionUserId = context?.userPayload?.userRecord?.ID;
|
|
101
|
+
if (!connectionUserId || !payload.ownerUserId) {
|
|
102
|
+
return false; // fail closed
|
|
103
|
+
}
|
|
104
|
+
return UUIDsEqual(payload.ownerUserId, connectionUserId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* MJServer's `TaskGraphObserver` — turns dispatcher frames into a PubSub publish.
|
|
109
|
+
*
|
|
110
|
+
* `OnFrame` is synchronous and swallows everything by contract: a frame is commentary on work, never
|
|
111
|
+
* a step of it, so nothing here may stall or fail a graph. The publish itself is fire-and-forget for
|
|
112
|
+
* the same reason.
|
|
113
|
+
*/
|
|
114
|
+
export class TaskGraphFrameBroadcaster implements TaskGraphObserver {
|
|
115
|
+
constructor(private readonly pubSub: PubSubEngine) {}
|
|
116
|
+
|
|
117
|
+
public OnFrame(frame: TaskGraphFrame): void {
|
|
118
|
+
try {
|
|
119
|
+
if (!frame.OwnerUserID) {
|
|
120
|
+
// A graph with no recorded owner cannot be authorized to anyone, so publishing would only
|
|
121
|
+
// produce frames the filter refuses. Dropping is the same outcome, without the traffic.
|
|
122
|
+
// Reachable for graphs submitted before ownership was recorded on the parent.
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
void this.pubSub.publish(
|
|
126
|
+
TASK_GRAPH_FRAMES_TOPIC,
|
|
127
|
+
{ ...frame, ownerUserId: frame.OwnerUserID } satisfies TaskGraphFramePayload,
|
|
128
|
+
);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
LogError(`[TaskGraphFrameBroadcaster] Could not publish ${frame.Kind} (ignored): ${e instanceof Error ? e.message : String(e)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Resolver()
|
|
136
|
+
export class TaskGraphFrameResolver {
|
|
137
|
+
@Subscription(() => TaskGraphFrameNotification, {
|
|
138
|
+
topics: TASK_GRAPH_FRAMES_TOPIC,
|
|
139
|
+
filter: (data: ResolverFilterData<TaskGraphFramePayload, TaskGraphFrameArgs, TaskGraphFrameFilterContext>) =>
|
|
140
|
+
taskGraphFrameFilter(data),
|
|
141
|
+
})
|
|
142
|
+
taskGraphFrames(
|
|
143
|
+
@Root() payload: TaskGraphFramePayload,
|
|
144
|
+
@Arg('parentTaskId', () => ID) parentTaskId: string
|
|
145
|
+
): TaskGraphFrameNotification {
|
|
146
|
+
// ownerUserId is intentionally NOT returned — it is a server-side filter key only.
|
|
147
|
+
return {
|
|
148
|
+
kind: payload.Kind,
|
|
149
|
+
parentTaskId,
|
|
150
|
+
taskId: payload.TaskID,
|
|
151
|
+
taskName: payload.TaskName,
|
|
152
|
+
status: payload.Status,
|
|
153
|
+
errorMessage: payload.ErrorMessage,
|
|
154
|
+
assignedUserId: payload.AssignedUserID,
|
|
155
|
+
completedCount: payload.CompletedCount,
|
|
156
|
+
totalCount: payload.TotalCount,
|
|
157
|
+
date: new Date(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
Query,
|
|
15
15
|
Resolver,
|
|
16
16
|
} from '@memberjunction/server';
|
|
17
|
-
import { UserCache } from '@memberjunction/
|
|
17
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
18
18
|
|
|
19
19
|
import { MJUserFavorite_, MJUserFavoriteResolverBase } from '../generated/generated.js';
|
|
20
20
|
import { GetReadOnlyProvider } from '../util.js';
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import express from 'express';
|
|
16
16
|
import { LogError, LogStatus, RunView, UserInfo } from '@memberjunction/core';
|
|
17
17
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
18
|
-
import { UserCache } from '@memberjunction/
|
|
18
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
19
19
|
import { OAuthManager, MCPClientManager } from '@memberjunction/ai-mcp-client';
|
|
20
20
|
import type { MCPServerOAuthConfig } from '@memberjunction/ai-mcp-client';
|
|
21
21
|
|
|
@@ -189,8 +189,7 @@ export class RESTEndpointHandler {
|
|
|
189
189
|
// Transaction operations
|
|
190
190
|
this.router.post('/transactions', this.executeTransaction.bind(this));
|
|
191
191
|
|
|
192
|
-
//
|
|
193
|
-
this.router.get('/reports/:reportId', this.runReport.bind(this));
|
|
192
|
+
// Queries
|
|
194
193
|
this.router.post('/queries/run', this.runQuery.bind(this));
|
|
195
194
|
|
|
196
195
|
// Error handling
|
|
@@ -781,19 +780,6 @@ export class RESTEndpointHandler {
|
|
|
781
780
|
}
|
|
782
781
|
}
|
|
783
782
|
|
|
784
|
-
/**
|
|
785
|
-
* Run a report
|
|
786
|
-
*/
|
|
787
|
-
private async runReport(req: express.Request, res: express.Response): Promise<void> {
|
|
788
|
-
try {
|
|
789
|
-
// Placeholder implementation - this would need to be implemented to run reports
|
|
790
|
-
res.status(501).json({ error: 'Not implemented' });
|
|
791
|
-
} catch (error) {
|
|
792
|
-
LogError(error);
|
|
793
|
-
res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
783
|
/**
|
|
798
784
|
* Run a query
|
|
799
785
|
*/
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import express from 'express';
|
|
15
15
|
import BodyParser from 'body-parser';
|
|
16
16
|
import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
|
|
17
|
-
import { UserCache } from '@memberjunction/
|
|
17
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
18
18
|
import { SignatureEngine } from '@memberjunction/esignature/server';
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MJServer's implementation of the `DurableEntityActionSubmitter` seam.
|
|
3
|
+
*
|
|
4
|
+
* Turns one After* entity-action dispatch into a single-node durable task graph, which is D14's
|
|
5
|
+
* whole answer to "run this durably": not a new queue, the substrate that already has a claim
|
|
6
|
+
* protocol, restart recovery and orphan reclaim.
|
|
7
|
+
*
|
|
8
|
+
* Registered at boot beside the dispatcher, because the two are one capability — a submitter with
|
|
9
|
+
* no dispatcher writes rows nobody picks up, which is worse than staying inline.
|
|
10
|
+
*
|
|
11
|
+
* @module @memberjunction/server
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
DurableEntityActionRequest,
|
|
15
|
+
DurableEntityActionSubmission,
|
|
16
|
+
DurableEntityActionSubmitter,
|
|
17
|
+
} from '@memberjunction/actions-base';
|
|
18
|
+
import { LogError } from '@memberjunction/core';
|
|
19
|
+
import { TaskNode, type TaskGraphSpec } from '@memberjunction/ai-core-plus';
|
|
20
|
+
import { MJEnvironmentEntityExtended } from '@memberjunction/core-entities';
|
|
21
|
+
import { TaskGraphService, type ProviderFactory } from '@memberjunction/task-graph';
|
|
22
|
+
|
|
23
|
+
export class DurableEntityActionTaskSubmitter implements DurableEntityActionSubmitter {
|
|
24
|
+
/**
|
|
25
|
+
* @param environmentID which environment the tasks belong to. Defaults to the platform default
|
|
26
|
+
* because a save has no environment of its own to inherit — the record's entity is global, and
|
|
27
|
+
* inventing a per-save environment would put a durable dispatch somewhere nobody looks.
|
|
28
|
+
*/
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly providerFactory: ProviderFactory,
|
|
31
|
+
private readonly environmentID: string = MJEnvironmentEntityExtended.DefaultEnvironmentID,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
public async Submit(request: DurableEntityActionRequest): Promise<DurableEntityActionSubmission> {
|
|
35
|
+
try {
|
|
36
|
+
const spec: TaskGraphSpec = {
|
|
37
|
+
workflowName: `${request.ActionName} — ${request.EntityName}`,
|
|
38
|
+
reasoning:
|
|
39
|
+
`Durable dispatch of entity action binding ${request.EntityActionID} on ` +
|
|
40
|
+
`${request.EntityName} (${request.RecordID}), fired by ${request.InvocationType}.`,
|
|
41
|
+
tasks: [
|
|
42
|
+
TaskNode.Action(
|
|
43
|
+
{
|
|
44
|
+
tempId: 'action',
|
|
45
|
+
name: request.ActionName,
|
|
46
|
+
description: `Run ${request.ActionName} for ${request.EntityName} ${request.RecordID}.`,
|
|
47
|
+
dependsOn: [],
|
|
48
|
+
inputPayload: request.RedactedParams,
|
|
49
|
+
},
|
|
50
|
+
{ actionName: request.ActionName },
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
// Nothing to say to anyone: this graph exists because a record was saved, not
|
|
54
|
+
// because a person asked a question. Defaulting to 'message' would post an
|
|
55
|
+
// orphaned result into whichever conversation happened to be nearby.
|
|
56
|
+
continuation: 'none',
|
|
57
|
+
// The point of the exercise. A one-node graph constant-folds to in-run execution by
|
|
58
|
+
// default (D9), which is exactly the fire-and-forget behaviour being escaped.
|
|
59
|
+
durable: true,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const provider = await this.providerFactory.CreateProvider();
|
|
63
|
+
const result = await new TaskGraphService().Submit(spec, {
|
|
64
|
+
EnvironmentID: this.environmentID,
|
|
65
|
+
ContextUser: request.ContextUser,
|
|
66
|
+
Provider: provider,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return result.Success
|
|
70
|
+
? { Success: true, ParentTaskID: result.ParentTaskID }
|
|
71
|
+
: { Success: false, ErrorMessage: result.ErrorMessage };
|
|
72
|
+
} catch (e) {
|
|
73
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
74
|
+
LogError(`[DurableEntityActionTaskSubmitter] Submission failed for binding ${request.EntityActionID}: ${message}`);
|
|
75
|
+
return { Success: false, ErrorMessage: message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Boots the durable task-graph dispatcher inside MJServer.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 landed the dispatcher and the submission path but never started anything, so a submitted
|
|
5
|
+
* graph persisted correctly and then sat in `Pending` forever. Submission being durable is only
|
|
6
|
+
* half the promise — something has to *pick the work up*.
|
|
7
|
+
*
|
|
8
|
+
* The continuation deliverer is supplied here for the same reason the agent runner is: posting into
|
|
9
|
+
* a conversation is a host concern the package deliberately refuses to know about.
|
|
10
|
+
*
|
|
11
|
+
* Registration of the package's Remote Operations is a separate concern handled by the generated
|
|
12
|
+
* class-registration manifests (`ServerBootstrap` / `ServerBootstrapLite`), which import the
|
|
13
|
+
* subclasses so their `@RegisterClass` decorators run. `LoadTaskGraphOperations` is called here
|
|
14
|
+
* anyway as a cheap guard: a host that starts a dispatcher but never loaded a manifest would accept
|
|
15
|
+
* submissions that route to the CodeGen contract-only base and silently do nothing.
|
|
16
|
+
*
|
|
17
|
+
* @module @memberjunction/server
|
|
18
|
+
*/
|
|
19
|
+
import { UserInfo } from '@memberjunction/core';
|
|
20
|
+
import { LoadTaskGraphOperations, TaskGraphDispatcher } from '@memberjunction/task-graph';
|
|
21
|
+
import { TaskGraphPromptRunner } from './TaskGraphPromptRunner.js';
|
|
22
|
+
import sql from 'mssql';
|
|
23
|
+
import { DurableEntityActionRegistry } from '@memberjunction/actions-base';
|
|
24
|
+
import { CreateTaskGraphProviderFactory } from './TaskGraphProviderFactory.js';
|
|
25
|
+
import { TaskGraphActionRunner } from './TaskGraphActionRunner.js';
|
|
26
|
+
import { TaskGraphAgentRunner } from './TaskGraphAgentRunner.js';
|
|
27
|
+
import { DurableEntityActionTaskSubmitter } from './DurableEntityActionTaskSubmitter.js';
|
|
28
|
+
import { TaskGraphContinuationDeliverer } from './TaskGraphContinuationDeliverer.js';
|
|
29
|
+
import { PubSubManager } from '../generic/PubSubManager.js';
|
|
30
|
+
import { TaskGraphFrameBroadcaster } from '../resolvers/TaskGraphFrameResolver.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Starts one dispatcher for this process and returns it.
|
|
34
|
+
*
|
|
35
|
+
* The instance ID must be stable for the process lifetime and distinct per instance — it is what
|
|
36
|
+
* lets reconciliation tell "my own orphaned work, reclaim it" from "another live instance's work,
|
|
37
|
+
* leave it alone". `pid` plus hostname satisfies both: stable while the process lives, and never
|
|
38
|
+
* reused by a peer.
|
|
39
|
+
*
|
|
40
|
+
* The dispatcher self-registers with `ShutdownRegistry`, so the caller does not wire teardown.
|
|
41
|
+
*/
|
|
42
|
+
export async function StartTaskGraphDispatcher(
|
|
43
|
+
pool: sql.ConnectionPool,
|
|
44
|
+
contextUser: UserInfo,
|
|
45
|
+
instanceID?: string,
|
|
46
|
+
): Promise<TaskGraphDispatcher> {
|
|
47
|
+
LoadTaskGraphOperations();
|
|
48
|
+
|
|
49
|
+
const providerFactory = CreateTaskGraphProviderFactory(pool);
|
|
50
|
+
const pubSub = PubSubManager.Instance.PubSubEngine;
|
|
51
|
+
const dispatcher = new TaskGraphDispatcher(
|
|
52
|
+
providerFactory,
|
|
53
|
+
new TaskGraphAgentRunner(),
|
|
54
|
+
contextUser,
|
|
55
|
+
{ InstanceID: instanceID ?? `${process.env.HOSTNAME ?? 'mjapi'}-${process.pid}` },
|
|
56
|
+
// Without this the dispatcher had nowhere to deliver: a finished graph logged its outcome,
|
|
57
|
+
// marked itself delivered, and said nothing to the conversation that asked for it.
|
|
58
|
+
new TaskGraphContinuationDeliverer(providerFactory, contextUser),
|
|
59
|
+
// Live signal. Supplied only when a PubSub engine exists — in a worker or a test there is
|
|
60
|
+
// nobody to publish to, and the dispatcher runs identically without one.
|
|
61
|
+
pubSub ? new TaskGraphFrameBroadcaster(pubSub) : undefined,
|
|
62
|
+
// Action nodes — the third execution shape, beside agents and people. Durable After* entity
|
|
63
|
+
// actions land as these.
|
|
64
|
+
new TaskGraphActionRunner(),
|
|
65
|
+
// Prompt nodes. Without this a Flow agent with a Prompt step submits and then
|
|
66
|
+
// stalls on a task nothing can execute — which is why the shipped User Onboarding
|
|
67
|
+
// Flow Agent could not run at all before this seam existed.
|
|
68
|
+
new TaskGraphPromptRunner(),
|
|
69
|
+
);
|
|
70
|
+
await dispatcher.Start();
|
|
71
|
+
|
|
72
|
+
// Registered only AFTER the dispatcher is running, and only here. A submitter without a
|
|
73
|
+
// dispatcher writes Task rows nobody picks up — strictly worse than staying inline, because the
|
|
74
|
+
// work is now both undone and invisible in the place it used to be logged.
|
|
75
|
+
DurableEntityActionRegistry.Instance.Register(new DurableEntityActionTaskSubmitter(providerFactory));
|
|
76
|
+
|
|
77
|
+
return dispatcher;
|
|
78
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MJServer's implementation of the task-graph `TaskAgentRunner` seam.
|
|
3
|
+
*
|
|
4
|
+
* The dispatcher knows *when* a task should run and *what* it depends on, but nothing about how an
|
|
5
|
+
* agent executes. That is injected, so the package stays testable without standing up the agent
|
|
6
|
+
* framework and so a host with a different execution strategy (a queue, a remote worker) can
|
|
7
|
+
* substitute its own.
|
|
8
|
+
*
|
|
9
|
+
* This adapter is the thin translation between the two: it turns a task's input payload and its
|
|
10
|
+
* dependencies' outputs into agent messages, runs the agent, and maps the result back into the
|
|
11
|
+
* shape the dispatcher persists.
|
|
12
|
+
*
|
|
13
|
+
* @module @memberjunction/server
|
|
14
|
+
*/
|
|
15
|
+
import { AgentRunner } from '@memberjunction/ai-agents';
|
|
16
|
+
import { ChatMessageRole } from '@memberjunction/ai';
|
|
17
|
+
import { LogError } from '@memberjunction/core';
|
|
18
|
+
import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended } from '@memberjunction/ai-core-plus';
|
|
19
|
+
import type { TaskAgentRunner, TaskAgentRunParams, TaskAgentRunResult } from '@memberjunction/task-graph';
|
|
20
|
+
|
|
21
|
+
export class TaskGraphAgentRunner implements TaskAgentRunner {
|
|
22
|
+
public async RunAgentForTask(params: TaskAgentRunParams): Promise<TaskAgentRunResult> {
|
|
23
|
+
try {
|
|
24
|
+
const agent = await params.Provider.GetEntityObject<MJAIAgentEntityExtended>('MJ: AI Agents', params.ContextUser);
|
|
25
|
+
if (!(await agent.Load(params.AgentID))) {
|
|
26
|
+
return { Success: false, ErrorMessage: `Agent ${params.AgentID} could not be loaded.` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = await new AgentRunner().RunAgent({
|
|
30
|
+
agent,
|
|
31
|
+
conversationMessages: [{ role: ChatMessageRole.user, content: this.buildPrompt(params) }],
|
|
32
|
+
contextUser: params.ContextUser,
|
|
33
|
+
// Inherited from the graph, so a graph this run goes on to emit is one hop deeper
|
|
34
|
+
// rather than starting the chain over. Without it MAX_REINVOKE_DEPTH never fires.
|
|
35
|
+
continuationDepth: params.ContinuationDepth ?? 0,
|
|
36
|
+
parentRun: await this.loadSubmittingRun(params),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const success = result?.success === true;
|
|
40
|
+
return {
|
|
41
|
+
Success: success,
|
|
42
|
+
Output: this.extractOutput(result),
|
|
43
|
+
ErrorMessage: success ? undefined : (result?.agentRun?.ErrorMessage ?? 'Agent execution failed'),
|
|
44
|
+
AgentRunID: result?.agentRun?.ID,
|
|
45
|
+
};
|
|
46
|
+
} catch (e) {
|
|
47
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
48
|
+
LogError(`[TaskGraphAgentRunner] Task ${params.TaskID} failed: ${message}`);
|
|
49
|
+
return { Success: false, ErrorMessage: message };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The run that submitted this graph, as the entity `RunAgent` wants for `parentRun`.
|
|
55
|
+
*
|
|
56
|
+
* **Why this link is worth a load.** It sets `ParentRunID` on the spawned run, which turns a
|
|
57
|
+
* workflow's total cost into one indexed sum over `RootParentRunID` rather than a walk of the
|
|
58
|
+
* graph, and gives the run a "started by" trail someone can follow back. Without it every
|
|
59
|
+
* dispatched run looks like it began from nowhere.
|
|
60
|
+
*
|
|
61
|
+
* Undefined — never a throw — when there is no submitting run or it cannot be read. A graph
|
|
62
|
+
* submitted by a schedule, by MCP, or by a person genuinely has no parent run, and failing the
|
|
63
|
+
* step because its provenance is unavailable would trade real work for a bookkeeping detail.
|
|
64
|
+
*/
|
|
65
|
+
private async loadSubmittingRun(params: TaskAgentRunParams): Promise<MJAIAgentRunEntityExtended | undefined> {
|
|
66
|
+
if (!params.SubmittingAgentRunID) return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const run = await params.Provider.GetEntityObject<MJAIAgentRunEntityExtended>(
|
|
69
|
+
'MJ: AI Agent Runs',
|
|
70
|
+
params.ContextUser,
|
|
71
|
+
);
|
|
72
|
+
return (await run.Load(params.SubmittingAgentRunID)) ? run : undefined;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
LogError(
|
|
75
|
+
`[TaskGraphAgentRunner] Task ${params.TaskID}: submitting run ` +
|
|
76
|
+
`${params.SubmittingAgentRunID} could not be loaded; the spawned run will have no ` +
|
|
77
|
+
`parent link. ${e instanceof Error ? e.message : String(e)}`,
|
|
78
|
+
);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Builds the agent's instruction from the task's own input plus its dependencies' outputs.
|
|
85
|
+
*
|
|
86
|
+
* Dependency outputs are included inline because a task's whole reason for depending on another
|
|
87
|
+
* is to consume what it produced. They are rendered as labelled JSON rather than prose so the
|
|
88
|
+
* agent can parse them reliably rather than inferring structure from formatting.
|
|
89
|
+
*/
|
|
90
|
+
private buildPrompt(params: TaskAgentRunParams): string {
|
|
91
|
+
const sections: string[] = [];
|
|
92
|
+
|
|
93
|
+
if (params.InputPayload != null) {
|
|
94
|
+
sections.push(`## Task input\n\`\`\`json\n${JSON.stringify(params.InputPayload, null, 2)}\n\`\`\``);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (params.DependencyOutputs.size > 0) {
|
|
98
|
+
const rendered = [...params.DependencyOutputs.entries()]
|
|
99
|
+
.map(([taskID, output]) => `### Output of task ${taskID}\n\`\`\`json\n${JSON.stringify(output, null, 2)}\n\`\`\``)
|
|
100
|
+
.join('\n\n');
|
|
101
|
+
sections.push(`## Results from prerequisite tasks\n\n${rendered}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return sections.length > 0
|
|
105
|
+
? sections.join('\n\n')
|
|
106
|
+
: 'Execute this task.';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Prefers a structured payload over prose, so downstream tasks get data rather than text. */
|
|
110
|
+
private extractOutput(result: unknown): unknown {
|
|
111
|
+
const r = result as { payload?: unknown; agentRun?: { Message?: string } } | null;
|
|
112
|
+
if (r?.payload != null) return r.payload;
|
|
113
|
+
return r?.agentRun?.Message ?? null;
|
|
114
|
+
}
|
|
115
|
+
}
|