@donkeylabs/server 2.0.20 → 2.0.22
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/docs/workflows.md +73 -7
- package/package.json +2 -2
- package/src/admin/dashboard.ts +74 -3
- package/src/admin/routes.ts +62 -0
- package/src/core/cron.ts +17 -10
- package/src/core/index.ts +28 -0
- package/src/core/jobs.ts +8 -2
- package/src/core/logger.ts +14 -0
- package/src/core/logs-adapter-kysely.ts +287 -0
- package/src/core/logs-transport.ts +83 -0
- package/src/core/logs.ts +398 -0
- package/src/core/workflow-executor.ts +116 -337
- package/src/core/workflow-socket.ts +2 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +399 -0
- package/src/core/workflows.ts +300 -909
- package/src/core.ts +2 -0
- package/src/harness.ts +4 -0
- package/src/index.ts +10 -0
- package/src/server.ts +44 -5
- /package/{CLAUDE.md → agents.md} +0 -0
|
@@ -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)
|
|
47
|
+
const sqlite = new Database(dbPath);
|
|
48
|
+
sqlite.run("PRAGMA busy_timeout = 5000");
|
|
55
49
|
const db = new Kysely<any>({
|
|
56
|
-
dialect: new BunSqliteDialect({
|
|
57
|
-
database: new Database(dbPath),
|
|
58
|
-
}),
|
|
50
|
+
dialect: new BunSqliteDialect({ database: sqlite }),
|
|
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,32 @@ 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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
79
|
+
// Wrap coreServices proxy so that `db` resolves locally instead of via IPC.
|
|
80
|
+
// Spreading a Proxy with no ownKeys trap loses all proxied properties.
|
|
81
|
+
const coreWithDb = new Proxy(coreServices, {
|
|
82
|
+
get(target, prop, receiver) {
|
|
83
|
+
if (prop === "db") return db;
|
|
84
|
+
return Reflect.get(target, prop, receiver);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Create state machine with IPC event bridge
|
|
89
|
+
const sm = new WorkflowStateMachine({
|
|
90
|
+
adapter,
|
|
91
|
+
core: coreWithDb as any,
|
|
107
92
|
plugins,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
93
|
+
events: createIpcEventBridge(socket, instanceId),
|
|
94
|
+
pollInterval: 1000,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Run the state machine to completion
|
|
98
|
+
const result = await sm.run(instanceId, definition);
|
|
112
99
|
|
|
113
100
|
// Send completed event
|
|
114
101
|
sendEvent(socket, {
|
|
@@ -127,8 +114,10 @@ async function main(): Promise<void> {
|
|
|
127
114
|
});
|
|
128
115
|
process.exit(1);
|
|
129
116
|
} finally {
|
|
117
|
+
clearInterval(heartbeatInterval);
|
|
130
118
|
proxyConnection.close();
|
|
131
119
|
socket.end();
|
|
120
|
+
adapter.stop();
|
|
132
121
|
await db.destroy();
|
|
133
122
|
}
|
|
134
123
|
|
|
@@ -136,320 +125,110 @@ async function main(): Promise<void> {
|
|
|
136
125
|
}
|
|
137
126
|
|
|
138
127
|
// ============================================
|
|
139
|
-
//
|
|
128
|
+
// IPC Event Bridge
|
|
140
129
|
// ============================================
|
|
141
130
|
|
|
142
|
-
function
|
|
143
|
-
return
|
|
144
|
-
|
|
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
|
|
162
|
-
// ============================================
|
|
163
|
-
|
|
164
|
-
async function executeWorkflow(
|
|
165
|
-
socket: Socket,
|
|
166
|
-
proxyConnection: WorkflowProxyConnection,
|
|
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
|
|
131
|
+
function createIpcEventBridge(socket: Socket, instanceId: string): StateMachineEvents {
|
|
132
|
+
return {
|
|
133
|
+
onStepStarted: (id, stepName, stepType) => {
|
|
198
134
|
sendEvent(socket, {
|
|
199
135
|
type: "step.started",
|
|
200
|
-
instanceId,
|
|
136
|
+
instanceId: id,
|
|
201
137
|
timestamp: Date.now(),
|
|
202
|
-
stepName
|
|
138
|
+
stepName,
|
|
139
|
+
stepType,
|
|
203
140
|
});
|
|
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
|
-
|
|
141
|
+
},
|
|
142
|
+
onStepCompleted: (id, stepName, output, nextStep) => {
|
|
284
143
|
sendEvent(socket, {
|
|
285
144
|
type: "step.completed",
|
|
286
|
-
instanceId,
|
|
145
|
+
instanceId: id,
|
|
287
146
|
timestamp: Date.now(),
|
|
288
|
-
stepName
|
|
147
|
+
stepName,
|
|
289
148
|
output,
|
|
290
|
-
nextStep
|
|
149
|
+
nextStep,
|
|
291
150
|
});
|
|
292
|
-
|
|
151
|
+
},
|
|
152
|
+
onStepFailed: (id, stepName, error, attempts) => {
|
|
153
|
+
sendEvent(socket, {
|
|
154
|
+
type: "step.failed",
|
|
155
|
+
instanceId: id,
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
stepName,
|
|
158
|
+
error,
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
onStepRetry: () => {
|
|
162
|
+
// Retry is internal to the state machine - no IPC event needed
|
|
163
|
+
},
|
|
164
|
+
onProgress: (id, progress, currentStep, completed, total) => {
|
|
293
165
|
sendEvent(socket, {
|
|
294
166
|
type: "progress",
|
|
295
|
-
instanceId,
|
|
167
|
+
instanceId: id,
|
|
296
168
|
timestamp: Date.now(),
|
|
297
169
|
progress,
|
|
298
|
-
completedSteps,
|
|
299
|
-
totalSteps,
|
|
170
|
+
completedSteps: completed,
|
|
171
|
+
totalSteps: total,
|
|
300
172
|
});
|
|
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
|
-
}
|
|
173
|
+
},
|
|
174
|
+
onCompleted: () => {
|
|
175
|
+
// Handled by the main try/catch after sm.run() returns
|
|
176
|
+
},
|
|
177
|
+
onFailed: () => {
|
|
178
|
+
// Handled by the main try/catch after sm.run() throws
|
|
179
|
+
},
|
|
180
|
+
};
|
|
333
181
|
}
|
|
334
182
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
183
|
+
// ============================================
|
|
184
|
+
// Socket Connection
|
|
185
|
+
// ============================================
|
|
339
186
|
|
|
340
|
-
|
|
187
|
+
function connectToSocket(socketPath?: string, tcpPort?: number): Promise<Socket> {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
let socket: Socket;
|
|
341
190
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
191
|
+
if (socketPath) {
|
|
192
|
+
socket = connect(socketPath);
|
|
193
|
+
} else if (tcpPort) {
|
|
194
|
+
socket = connect(tcpPort, "127.0.0.1");
|
|
346
195
|
} 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}`);
|
|
196
|
+
reject(new Error("No socket path or TCP port provided"));
|
|
197
|
+
return;
|
|
366
198
|
}
|
|
367
|
-
result = parseResult.data;
|
|
368
|
-
}
|
|
369
199
|
|
|
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;
|
|
200
|
+
socket.once("connect", () => resolve(socket));
|
|
201
|
+
socket.once("error", (err) => reject(err));
|
|
202
|
+
});
|
|
381
203
|
}
|
|
382
204
|
|
|
383
205
|
// ============================================
|
|
384
|
-
//
|
|
206
|
+
// Helpers
|
|
385
207
|
// ============================================
|
|
386
208
|
|
|
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;
|
|
209
|
+
function sendEvent(socket: Socket, event: WorkflowEvent): void {
|
|
210
|
+
socket.write(JSON.stringify(event) + "\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function findWorkflowDefinition(
|
|
214
|
+
module: any,
|
|
215
|
+
workflowName: string,
|
|
216
|
+
modulePath: string,
|
|
217
|
+
): WorkflowDefinition {
|
|
218
|
+
// Try named exports
|
|
219
|
+
for (const key of Object.keys(module)) {
|
|
220
|
+
const exported = module[key];
|
|
221
|
+
if (isWorkflowDefinition(exported) && exported.name === workflowName) {
|
|
222
|
+
return exported;
|
|
403
223
|
}
|
|
404
224
|
}
|
|
405
225
|
|
|
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
|
-
// ============================================
|
|
226
|
+
// Try default export
|
|
227
|
+
if (module.default && isWorkflowDefinition(module.default) && module.default.name === workflowName) {
|
|
228
|
+
return module.default;
|
|
229
|
+
}
|
|
450
230
|
|
|
451
|
-
|
|
452
|
-
socket.write(JSON.stringify(event) + "\n");
|
|
231
|
+
throw new Error(`Workflow "${workflowName}" not found in module ${modulePath}`);
|
|
453
232
|
}
|
|
454
233
|
|
|
455
234
|
function isWorkflowDefinition(obj: any): obj is WorkflowDefinition {
|