@donkeylabs/server 2.0.20 → 2.0.21
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/package.json +1 -1
- package/src/core/index.ts +6 -0
- package/src/core/workflow-executor.ts +104 -334
- package/src/core/workflow-socket.ts +2 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +343 -0
- package/src/core/workflows.ts +234 -887
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -217,6 +217,12 @@ export {
|
|
|
217
217
|
createProcessSocketServer,
|
|
218
218
|
} from "./process-socket";
|
|
219
219
|
|
|
220
|
+
export {
|
|
221
|
+
WorkflowStateMachine,
|
|
222
|
+
type StateMachineEvents,
|
|
223
|
+
type StateMachineConfig,
|
|
224
|
+
} from "./workflow-state-machine";
|
|
225
|
+
|
|
220
226
|
export {
|
|
221
227
|
KyselyWorkflowAdapter,
|
|
222
228
|
type KyselyWorkflowAdapterConfig,
|
|
@@ -1,39 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Workflow Executor - Subprocess Entry Point
|
|
3
|
-
//
|
|
3
|
+
// Thin shell that creates a WorkflowStateMachine with IPC event bridge.
|
|
4
|
+
// The state machine owns all execution logic and persistence.
|
|
4
5
|
|
|
5
6
|
import { connect } from "node:net";
|
|
6
7
|
import type { Socket } from "node:net";
|
|
7
8
|
import { Kysely } from "kysely";
|
|
8
9
|
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
9
10
|
import Database from "bun:sqlite";
|
|
10
|
-
import type { WorkflowEvent
|
|
11
|
+
import type { WorkflowEvent } from "./workflow-socket";
|
|
11
12
|
import { WorkflowProxyConnection, createPluginsProxy, createCoreServicesProxy } from "./workflow-proxy";
|
|
12
|
-
import type { WorkflowDefinition
|
|
13
|
+
import type { WorkflowDefinition } from "./workflows";
|
|
14
|
+
import { KyselyWorkflowAdapter } from "./workflow-adapter-kysely";
|
|
15
|
+
import { WorkflowStateMachine, type StateMachineEvents } from "./workflow-state-machine";
|
|
13
16
|
|
|
14
17
|
// ============================================
|
|
15
18
|
// Types
|
|
16
19
|
// ============================================
|
|
17
20
|
|
|
18
21
|
interface ExecutorConfig {
|
|
19
|
-
/** Workflow instance ID */
|
|
20
22
|
instanceId: string;
|
|
21
|
-
/** Workflow name (for importing the definition) */
|
|
22
23
|
workflowName: string;
|
|
23
|
-
/** Input data for the workflow */
|
|
24
24
|
input: any;
|
|
25
|
-
/** Unix socket path to connect to */
|
|
26
25
|
socketPath?: string;
|
|
27
|
-
/** TCP port for Windows */
|
|
28
26
|
tcpPort?: number;
|
|
29
|
-
/** Module path to import workflow definition from */
|
|
30
27
|
modulePath: string;
|
|
31
|
-
/** Database file path */
|
|
32
28
|
dbPath: string;
|
|
33
|
-
/** Initial step results (for resuming) */
|
|
34
|
-
stepResults?: Record<string, any>;
|
|
35
|
-
/** Current step name (for resuming) */
|
|
36
|
-
currentStep?: string;
|
|
37
29
|
}
|
|
38
30
|
|
|
39
31
|
// ============================================
|
|
@@ -45,18 +37,28 @@ async function main(): Promise<void> {
|
|
|
45
37
|
const stdin = await Bun.stdin.text();
|
|
46
38
|
const config: ExecutorConfig = JSON.parse(stdin);
|
|
47
39
|
|
|
48
|
-
const { instanceId, workflowName,
|
|
40
|
+
const { instanceId, workflowName, socketPath, tcpPort, modulePath, dbPath } = config;
|
|
49
41
|
|
|
50
42
|
// Connect to IPC socket
|
|
51
43
|
const socket = await connectToSocket(socketPath, tcpPort);
|
|
52
44
|
const proxyConnection = new WorkflowProxyConnection(socket);
|
|
53
45
|
|
|
54
|
-
// Create database connection
|
|
46
|
+
// Create database connection + adapter (subprocess owns its own persistence)
|
|
55
47
|
const db = new Kysely<any>({
|
|
56
48
|
dialect: new BunSqliteDialect({
|
|
57
49
|
database: new Database(dbPath),
|
|
58
50
|
}),
|
|
59
51
|
});
|
|
52
|
+
const adapter = new KyselyWorkflowAdapter(db, { cleanupDays: 0 });
|
|
53
|
+
|
|
54
|
+
// Start heartbeat
|
|
55
|
+
const heartbeatInterval = setInterval(() => {
|
|
56
|
+
sendEvent(socket, {
|
|
57
|
+
type: "heartbeat",
|
|
58
|
+
instanceId,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
}, 5000);
|
|
60
62
|
|
|
61
63
|
try {
|
|
62
64
|
// Send started event
|
|
@@ -68,47 +70,23 @@ async function main(): Promise<void> {
|
|
|
68
70
|
|
|
69
71
|
// Import the workflow module to get the definition
|
|
70
72
|
const module = await import(modulePath);
|
|
73
|
+
const definition = findWorkflowDefinition(module, workflowName, modulePath);
|
|
71
74
|
|
|
72
|
-
//
|
|
73
|
-
let definition: WorkflowDefinition | undefined;
|
|
74
|
-
|
|
75
|
-
// Try common export patterns
|
|
76
|
-
for (const key of Object.keys(module)) {
|
|
77
|
-
const exported = module[key];
|
|
78
|
-
if (isWorkflowDefinition(exported) && exported.name === workflowName) {
|
|
79
|
-
definition = exported;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Also check default export
|
|
85
|
-
if (!definition && module.default) {
|
|
86
|
-
if (isWorkflowDefinition(module.default) && module.default.name === workflowName) {
|
|
87
|
-
definition = module.default;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!definition) {
|
|
92
|
-
throw new Error(`Workflow "${workflowName}" not found in module ${modulePath}`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Create proxy context for plugin/core access
|
|
75
|
+
// Create proxy objects for plugin/core access via IPC
|
|
96
76
|
const plugins = createPluginsProxy(proxyConnection);
|
|
97
77
|
const coreServices = createCoreServicesProxy(proxyConnection);
|
|
98
78
|
|
|
99
|
-
//
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
definition,
|
|
104
|
-
instanceId,
|
|
105
|
-
input,
|
|
106
|
-
db,
|
|
79
|
+
// Create state machine with IPC event bridge
|
|
80
|
+
const sm = new WorkflowStateMachine({
|
|
81
|
+
adapter,
|
|
82
|
+
core: { ...coreServices, db } as any,
|
|
107
83
|
plugins,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
events: createIpcEventBridge(socket, instanceId),
|
|
85
|
+
pollInterval: 1000,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Run the state machine to completion
|
|
89
|
+
const result = await sm.run(instanceId, definition);
|
|
112
90
|
|
|
113
91
|
// Send completed event
|
|
114
92
|
sendEvent(socket, {
|
|
@@ -127,8 +105,10 @@ async function main(): Promise<void> {
|
|
|
127
105
|
});
|
|
128
106
|
process.exit(1);
|
|
129
107
|
} finally {
|
|
108
|
+
clearInterval(heartbeatInterval);
|
|
130
109
|
proxyConnection.close();
|
|
131
110
|
socket.end();
|
|
111
|
+
adapter.stop();
|
|
132
112
|
await db.destroy();
|
|
133
113
|
}
|
|
134
114
|
|
|
@@ -136,320 +116,110 @@ async function main(): Promise<void> {
|
|
|
136
116
|
}
|
|
137
117
|
|
|
138
118
|
// ============================================
|
|
139
|
-
//
|
|
140
|
-
// ============================================
|
|
141
|
-
|
|
142
|
-
function connectToSocket(socketPath?: string, tcpPort?: number): Promise<Socket> {
|
|
143
|
-
return new Promise((resolve, reject) => {
|
|
144
|
-
let socket: Socket;
|
|
145
|
-
|
|
146
|
-
if (socketPath) {
|
|
147
|
-
socket = connect(socketPath);
|
|
148
|
-
} else if (tcpPort) {
|
|
149
|
-
socket = connect(tcpPort, "127.0.0.1");
|
|
150
|
-
} else {
|
|
151
|
-
reject(new Error("No socket path or TCP port provided"));
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
socket.once("connect", () => resolve(socket));
|
|
156
|
-
socket.once("error", (err) => reject(err));
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ============================================
|
|
161
|
-
// Workflow Execution
|
|
119
|
+
// IPC Event Bridge
|
|
162
120
|
// ============================================
|
|
163
121
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
definition: WorkflowDefinition,
|
|
168
|
-
instanceId: string,
|
|
169
|
-
input: any,
|
|
170
|
-
db: Kysely<any>,
|
|
171
|
-
plugins: Record<string, any>,
|
|
172
|
-
coreServices: Record<string, any>,
|
|
173
|
-
initialStepResults: Record<string, any>,
|
|
174
|
-
startStep: string
|
|
175
|
-
): Promise<any> {
|
|
176
|
-
const stepResults: Record<string, any> = { ...initialStepResults };
|
|
177
|
-
let currentStepName: string | undefined = startStep;
|
|
178
|
-
let lastOutput: any;
|
|
179
|
-
let heartbeatInterval: ReturnType<typeof setInterval> | undefined;
|
|
180
|
-
|
|
181
|
-
// Start heartbeat
|
|
182
|
-
heartbeatInterval = setInterval(() => {
|
|
183
|
-
sendEvent(socket, {
|
|
184
|
-
type: "heartbeat",
|
|
185
|
-
instanceId,
|
|
186
|
-
timestamp: Date.now(),
|
|
187
|
-
});
|
|
188
|
-
}, 5000); // Every 5 seconds
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
while (currentStepName) {
|
|
192
|
-
const step = definition.steps.get(currentStepName);
|
|
193
|
-
if (!step) {
|
|
194
|
-
throw new Error(`Step "${currentStepName}" not found in workflow`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Send step started event
|
|
122
|
+
function createIpcEventBridge(socket: Socket, instanceId: string): StateMachineEvents {
|
|
123
|
+
return {
|
|
124
|
+
onStepStarted: (id, stepName, stepType) => {
|
|
198
125
|
sendEvent(socket, {
|
|
199
126
|
type: "step.started",
|
|
200
|
-
instanceId,
|
|
127
|
+
instanceId: id,
|
|
201
128
|
timestamp: Date.now(),
|
|
202
|
-
stepName
|
|
129
|
+
stepName,
|
|
130
|
+
stepType,
|
|
203
131
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const ctx = buildContext(
|
|
207
|
-
input,
|
|
208
|
-
stepResults,
|
|
209
|
-
lastOutput,
|
|
210
|
-
instanceId,
|
|
211
|
-
definition,
|
|
212
|
-
currentStepName,
|
|
213
|
-
db,
|
|
214
|
-
plugins,
|
|
215
|
-
coreServices
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// Execute step
|
|
219
|
-
let output: any;
|
|
220
|
-
try {
|
|
221
|
-
output = await executeStep(step, ctx);
|
|
222
|
-
} catch (error) {
|
|
223
|
-
// Check for retry config
|
|
224
|
-
const retry = step.retry ?? definition.defaultRetry;
|
|
225
|
-
const attempts = (stepResults[currentStepName]?.attempts ?? 0) + 1;
|
|
226
|
-
|
|
227
|
-
if (retry && attempts < retry.maxAttempts) {
|
|
228
|
-
// Retry logic
|
|
229
|
-
const backoffRate = retry.backoffRate ?? 2;
|
|
230
|
-
const intervalMs = retry.intervalMs ?? 1000;
|
|
231
|
-
const maxIntervalMs = retry.maxIntervalMs ?? 30000;
|
|
232
|
-
const delay = Math.min(
|
|
233
|
-
intervalMs * Math.pow(backoffRate, attempts - 1),
|
|
234
|
-
maxIntervalMs
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
stepResults[currentStepName] = {
|
|
238
|
-
stepName: currentStepName,
|
|
239
|
-
status: "pending",
|
|
240
|
-
attempts,
|
|
241
|
-
error: error instanceof Error ? error.message : String(error),
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
245
|
-
continue; // Retry the same step
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// No more retries, send step failed event
|
|
249
|
-
sendEvent(socket, {
|
|
250
|
-
type: "step.failed",
|
|
251
|
-
instanceId,
|
|
252
|
-
timestamp: Date.now(),
|
|
253
|
-
stepName: currentStepName,
|
|
254
|
-
error: error instanceof Error ? error.message : String(error),
|
|
255
|
-
});
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Store step result
|
|
260
|
-
stepResults[currentStepName] = {
|
|
261
|
-
stepName: currentStepName,
|
|
262
|
-
status: "completed",
|
|
263
|
-
output,
|
|
264
|
-
completedAt: new Date(),
|
|
265
|
-
attempts: (stepResults[currentStepName]?.attempts ?? 0) + 1,
|
|
266
|
-
};
|
|
267
|
-
lastOutput = output;
|
|
268
|
-
|
|
269
|
-
// Send step completed event
|
|
270
|
-
const completedSteps = Object.values(stepResults).filter(
|
|
271
|
-
(r: any) => r.status === "completed"
|
|
272
|
-
).length;
|
|
273
|
-
const totalSteps = definition.steps.size;
|
|
274
|
-
const progress = Math.round((completedSteps / totalSteps) * 100);
|
|
275
|
-
|
|
276
|
-
// Determine next step
|
|
277
|
-
let nextStepName: string | undefined;
|
|
278
|
-
if (step.end) {
|
|
279
|
-
nextStepName = undefined;
|
|
280
|
-
} else if (step.next) {
|
|
281
|
-
nextStepName = step.next;
|
|
282
|
-
}
|
|
283
|
-
|
|
132
|
+
},
|
|
133
|
+
onStepCompleted: (id, stepName, output, nextStep) => {
|
|
284
134
|
sendEvent(socket, {
|
|
285
135
|
type: "step.completed",
|
|
286
|
-
instanceId,
|
|
136
|
+
instanceId: id,
|
|
287
137
|
timestamp: Date.now(),
|
|
288
|
-
stepName
|
|
138
|
+
stepName,
|
|
289
139
|
output,
|
|
290
|
-
nextStep
|
|
140
|
+
nextStep,
|
|
291
141
|
});
|
|
292
|
-
|
|
142
|
+
},
|
|
143
|
+
onStepFailed: (id, stepName, error, attempts) => {
|
|
144
|
+
sendEvent(socket, {
|
|
145
|
+
type: "step.failed",
|
|
146
|
+
instanceId: id,
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
stepName,
|
|
149
|
+
error,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
onStepRetry: () => {
|
|
153
|
+
// Retry is internal to the state machine - no IPC event needed
|
|
154
|
+
},
|
|
155
|
+
onProgress: (id, progress, currentStep, completed, total) => {
|
|
293
156
|
sendEvent(socket, {
|
|
294
157
|
type: "progress",
|
|
295
|
-
instanceId,
|
|
158
|
+
instanceId: id,
|
|
296
159
|
timestamp: Date.now(),
|
|
297
160
|
progress,
|
|
298
|
-
completedSteps,
|
|
299
|
-
totalSteps,
|
|
161
|
+
completedSteps: completed,
|
|
162
|
+
totalSteps: total,
|
|
300
163
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return lastOutput;
|
|
313
|
-
} finally {
|
|
314
|
-
if (heartbeatInterval) {
|
|
315
|
-
clearInterval(heartbeatInterval);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async function executeStep(step: StepDefinition, ctx: WorkflowContext): Promise<any> {
|
|
321
|
-
switch (step.type) {
|
|
322
|
-
case "task":
|
|
323
|
-
return executeTaskStep(step as TaskStepDefinition, ctx);
|
|
324
|
-
case "pass":
|
|
325
|
-
return executePassStep(step, ctx);
|
|
326
|
-
case "choice":
|
|
327
|
-
throw new Error("Choice steps should be handled by main process flow");
|
|
328
|
-
case "parallel":
|
|
329
|
-
throw new Error("Parallel steps should be handled by main process");
|
|
330
|
-
default:
|
|
331
|
-
throw new Error(`Unknown step type: ${(step as any).type}`);
|
|
332
|
-
}
|
|
164
|
+
},
|
|
165
|
+
onCompleted: () => {
|
|
166
|
+
// Handled by the main try/catch after sm.run() returns
|
|
167
|
+
},
|
|
168
|
+
onFailed: () => {
|
|
169
|
+
// Handled by the main try/catch after sm.run() throws
|
|
170
|
+
},
|
|
171
|
+
};
|
|
333
172
|
}
|
|
334
173
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
174
|
+
// ============================================
|
|
175
|
+
// Socket Connection
|
|
176
|
+
// ============================================
|
|
339
177
|
|
|
340
|
-
|
|
178
|
+
function connectToSocket(socketPath?: string, tcpPort?: number): Promise<Socket> {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
let socket: Socket;
|
|
341
181
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
182
|
+
if (socketPath) {
|
|
183
|
+
socket = connect(socketPath);
|
|
184
|
+
} else if (tcpPort) {
|
|
185
|
+
socket = connect(tcpPort, "127.0.0.1");
|
|
346
186
|
} else {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (!parseResult.success) {
|
|
350
|
-
throw new Error(`Input validation failed: ${parseResult.error.message}`);
|
|
351
|
-
}
|
|
352
|
-
input = parseResult.data;
|
|
353
|
-
}
|
|
354
|
-
} else {
|
|
355
|
-
input = ctx.input;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Execute handler
|
|
359
|
-
let result = await step.handler(input, ctx);
|
|
360
|
-
|
|
361
|
-
// Validate output if schema provided
|
|
362
|
-
if (step.outputSchema) {
|
|
363
|
-
const parseResult = step.outputSchema.safeParse(result);
|
|
364
|
-
if (!parseResult.success) {
|
|
365
|
-
throw new Error(`Output validation failed: ${parseResult.error.message}`);
|
|
187
|
+
reject(new Error("No socket path or TCP port provided"));
|
|
188
|
+
return;
|
|
366
189
|
}
|
|
367
|
-
result = parseResult.data;
|
|
368
|
-
}
|
|
369
190
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
async function executePassStep(step: any, ctx: WorkflowContext): Promise<any> {
|
|
374
|
-
if (step.result !== undefined) {
|
|
375
|
-
return step.result;
|
|
376
|
-
}
|
|
377
|
-
if (step.transform) {
|
|
378
|
-
return step.transform(ctx);
|
|
379
|
-
}
|
|
380
|
-
return ctx.input;
|
|
191
|
+
socket.once("connect", () => resolve(socket));
|
|
192
|
+
socket.once("error", (err) => reject(err));
|
|
193
|
+
});
|
|
381
194
|
}
|
|
382
195
|
|
|
383
196
|
// ============================================
|
|
384
|
-
//
|
|
197
|
+
// Helpers
|
|
385
198
|
// ============================================
|
|
386
199
|
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if ((result as any).status === "completed" && (result as any).output !== undefined) {
|
|
402
|
-
steps[name] = (result as any).output;
|
|
200
|
+
function sendEvent(socket: Socket, event: WorkflowEvent): void {
|
|
201
|
+
socket.write(JSON.stringify(event) + "\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function findWorkflowDefinition(
|
|
205
|
+
module: any,
|
|
206
|
+
workflowName: string,
|
|
207
|
+
modulePath: string,
|
|
208
|
+
): WorkflowDefinition {
|
|
209
|
+
// Try named exports
|
|
210
|
+
for (const key of Object.keys(module)) {
|
|
211
|
+
const exported = module[key];
|
|
212
|
+
if (isWorkflowDefinition(exported) && exported.name === workflowName) {
|
|
213
|
+
return exported;
|
|
403
214
|
}
|
|
404
215
|
}
|
|
405
216
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
status: "running" as const,
|
|
411
|
-
currentStep,
|
|
412
|
-
input,
|
|
413
|
-
stepResults,
|
|
414
|
-
createdAt: new Date(),
|
|
415
|
-
startedAt: new Date(),
|
|
416
|
-
metadata: {},
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
// Metadata is stored locally - setMetadata sends via proxy
|
|
420
|
-
const metadata: Record<string, any> = {};
|
|
421
|
-
|
|
422
|
-
return {
|
|
423
|
-
input,
|
|
424
|
-
steps,
|
|
425
|
-
prev,
|
|
426
|
-
instance,
|
|
427
|
-
getStepResult: <T = any>(stepName: string): T | undefined => {
|
|
428
|
-
return steps[stepName] as T | undefined;
|
|
429
|
-
},
|
|
430
|
-
core: {
|
|
431
|
-
...coreServices,
|
|
432
|
-
db,
|
|
433
|
-
} as any,
|
|
434
|
-
plugins,
|
|
435
|
-
metadata,
|
|
436
|
-
setMetadata: async (key: string, value: any): Promise<void> => {
|
|
437
|
-
metadata[key] = value;
|
|
438
|
-
// Update via proxy to persist
|
|
439
|
-
await coreServices.workflows?.updateMetadata?.(instanceId, key, value);
|
|
440
|
-
},
|
|
441
|
-
getMetadata: <T = any>(key: string): T | undefined => {
|
|
442
|
-
return metadata[key] as T | undefined;
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ============================================
|
|
448
|
-
// Helpers
|
|
449
|
-
// ============================================
|
|
217
|
+
// Try default export
|
|
218
|
+
if (module.default && isWorkflowDefinition(module.default) && module.default.name === workflowName) {
|
|
219
|
+
return module.default;
|
|
220
|
+
}
|
|
450
221
|
|
|
451
|
-
|
|
452
|
-
socket.write(JSON.stringify(event) + "\n");
|
|
222
|
+
throw new Error(`Workflow "${workflowName}" not found in module ${modulePath}`);
|
|
453
223
|
}
|
|
454
224
|
|
|
455
225
|
function isWorkflowDefinition(obj: any): obj is WorkflowDefinition {
|