@donkeylabs/server 2.0.27 → 2.0.29
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/jobs.md +20 -0
- package/docs/processes.md +27 -0
- package/docs/workflows.md +69 -0
- package/package.json +1 -1
- package/src/admin/routes.ts +24 -0
- package/src/core/external-job-socket.ts +20 -1
- package/src/core/external-jobs.ts +4 -0
- package/src/core/index.ts +4 -0
- package/src/core/jobs.ts +96 -50
- package/src/core/process-socket.ts +20 -1
- package/src/core/processes.ts +95 -4
- package/src/core/subprocess-bootstrap.ts +14 -1
- package/src/core/workflow-adapter-kysely.ts +5 -0
- package/src/core/workflow-executor.ts +28 -0
- package/src/core/workflow-socket.ts +28 -2
- package/src/core/workflow-state-machine.ts +147 -2
- package/src/core/workflows.ts +260 -13
|
@@ -32,6 +32,11 @@ export interface SubprocessPluginMetadata {
|
|
|
32
32
|
export interface SubprocessBootstrapOptions {
|
|
33
33
|
dbPath: string;
|
|
34
34
|
coreConfig?: Record<string, any>;
|
|
35
|
+
sqlitePragmas?: {
|
|
36
|
+
busyTimeout?: number;
|
|
37
|
+
synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
|
|
38
|
+
journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
|
|
39
|
+
};
|
|
35
40
|
pluginMetadata: SubprocessPluginMetadata;
|
|
36
41
|
startServices?: {
|
|
37
42
|
cron?: boolean;
|
|
@@ -53,7 +58,15 @@ export async function bootstrapSubprocess(
|
|
|
53
58
|
options: SubprocessBootstrapOptions
|
|
54
59
|
): Promise<SubprocessBootstrapResult> {
|
|
55
60
|
const sqlite = new Database(options.dbPath);
|
|
56
|
-
|
|
61
|
+
const pragmas = options.sqlitePragmas;
|
|
62
|
+
const busyTimeout = pragmas?.busyTimeout ?? 5000;
|
|
63
|
+
sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
64
|
+
if (pragmas?.journalMode) {
|
|
65
|
+
sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
|
|
66
|
+
}
|
|
67
|
+
if (pragmas?.synchronous) {
|
|
68
|
+
sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
|
|
69
|
+
}
|
|
57
70
|
|
|
58
71
|
const db = new Kysely<any>({
|
|
59
72
|
dialect: new BunSqliteDialect({ database: sqlite }),
|
|
@@ -258,6 +258,11 @@ export class KyselyWorkflowAdapter implements WorkflowAdapter {
|
|
|
258
258
|
startedAt: sr.startedAt ? new Date(sr.startedAt) : undefined,
|
|
259
259
|
completedAt: sr.completedAt ? new Date(sr.completedAt) : undefined,
|
|
260
260
|
attempts: sr.attempts,
|
|
261
|
+
pollCount: sr.pollCount,
|
|
262
|
+
lastPolledAt: sr.lastPolledAt ? new Date(sr.lastPolledAt) : undefined,
|
|
263
|
+
loopCount: sr.loopCount,
|
|
264
|
+
lastLoopedAt: sr.lastLoopedAt ? new Date(sr.lastLoopedAt) : undefined,
|
|
265
|
+
loopStartedAt: sr.loopStartedAt ? new Date(sr.loopStartedAt) : undefined,
|
|
261
266
|
};
|
|
262
267
|
}
|
|
263
268
|
|
|
@@ -25,6 +25,11 @@ interface ExecutorConfig {
|
|
|
25
25
|
pluginModulePaths: Record<string, string>;
|
|
26
26
|
pluginConfigs: Record<string, any>;
|
|
27
27
|
coreConfig?: Record<string, any>;
|
|
28
|
+
sqlitePragmas?: {
|
|
29
|
+
busyTimeout?: number;
|
|
30
|
+
synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
|
|
31
|
+
journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
|
|
32
|
+
};
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
// ============================================
|
|
@@ -47,6 +52,7 @@ async function main(): Promise<void> {
|
|
|
47
52
|
pluginModulePaths,
|
|
48
53
|
pluginConfigs,
|
|
49
54
|
coreConfig,
|
|
55
|
+
sqlitePragmas,
|
|
50
56
|
} = config;
|
|
51
57
|
|
|
52
58
|
const socket = await connectToSocket(socketPath, tcpPort);
|
|
@@ -71,6 +77,7 @@ async function main(): Promise<void> {
|
|
|
71
77
|
const bootstrap = await bootstrapSubprocess({
|
|
72
78
|
dbPath,
|
|
73
79
|
coreConfig,
|
|
80
|
+
sqlitePragmas,
|
|
74
81
|
pluginMetadata: {
|
|
75
82
|
names: pluginNames,
|
|
76
83
|
modulePaths: pluginModulePaths,
|
|
@@ -184,6 +191,27 @@ function createIpcEventBridge(socket: Socket, instanceId: string): StateMachineE
|
|
|
184
191
|
error,
|
|
185
192
|
});
|
|
186
193
|
},
|
|
194
|
+
onStepPoll: (id, stepName, pollCount, done, result) => {
|
|
195
|
+
sendEvent(socket, {
|
|
196
|
+
type: "step.poll",
|
|
197
|
+
instanceId: id,
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
stepName,
|
|
200
|
+
pollCount,
|
|
201
|
+
done,
|
|
202
|
+
result,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
onStepLoop: (id, stepName, loopCount, target) => {
|
|
206
|
+
sendEvent(socket, {
|
|
207
|
+
type: "step.loop",
|
|
208
|
+
instanceId: id,
|
|
209
|
+
timestamp: Date.now(),
|
|
210
|
+
stepName,
|
|
211
|
+
loopCount,
|
|
212
|
+
target,
|
|
213
|
+
});
|
|
214
|
+
},
|
|
187
215
|
onStepRetry: () => {
|
|
188
216
|
// Retry is internal to the state machine - no IPC event needed
|
|
189
217
|
},
|
|
@@ -20,6 +20,8 @@ export type WorkflowEventType =
|
|
|
20
20
|
| "step.started"
|
|
21
21
|
| "step.completed"
|
|
22
22
|
| "step.failed"
|
|
23
|
+
| "step.poll"
|
|
24
|
+
| "step.loop"
|
|
23
25
|
| "progress"
|
|
24
26
|
| "completed"
|
|
25
27
|
| "failed"
|
|
@@ -41,6 +43,11 @@ export interface WorkflowEvent {
|
|
|
41
43
|
totalSteps?: number;
|
|
42
44
|
/** Next step to execute (for step.completed events) */
|
|
43
45
|
nextStep?: string;
|
|
46
|
+
pollCount?: number;
|
|
47
|
+
done?: boolean;
|
|
48
|
+
result?: any;
|
|
49
|
+
loopCount?: number;
|
|
50
|
+
target?: string;
|
|
44
51
|
/** Custom event name (for event type) */
|
|
45
52
|
event?: string;
|
|
46
53
|
/** Custom event payload or log data */
|
|
@@ -241,7 +248,24 @@ export class WorkflowSocketServerImpl implements WorkflowSocketServer {
|
|
|
241
248
|
|
|
242
249
|
let buffer = "";
|
|
243
250
|
|
|
244
|
-
|
|
251
|
+
const queue: WorkflowMessage[] = [];
|
|
252
|
+
let processing = false;
|
|
253
|
+
|
|
254
|
+
const processQueue = async () => {
|
|
255
|
+
if (processing) return;
|
|
256
|
+
processing = true;
|
|
257
|
+
while (queue.length > 0) {
|
|
258
|
+
const message = queue.shift()!;
|
|
259
|
+
try {
|
|
260
|
+
await this.handleMessage(instanceId, message);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
this.onError?.(err instanceof Error ? err : new Error(String(err)), instanceId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
processing = false;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
socket.on("data", (data) => {
|
|
245
269
|
buffer += data.toString();
|
|
246
270
|
|
|
247
271
|
// Process complete messages (newline-delimited JSON)
|
|
@@ -253,11 +277,13 @@ export class WorkflowSocketServerImpl implements WorkflowSocketServer {
|
|
|
253
277
|
|
|
254
278
|
try {
|
|
255
279
|
const message = JSON.parse(line) as WorkflowMessage;
|
|
256
|
-
|
|
280
|
+
queue.push(message);
|
|
257
281
|
} catch (err) {
|
|
258
282
|
this.onError?.(new Error(`Invalid message: ${line}`), instanceId);
|
|
259
283
|
}
|
|
260
284
|
}
|
|
285
|
+
|
|
286
|
+
processQueue().catch(() => undefined);
|
|
261
287
|
});
|
|
262
288
|
|
|
263
289
|
socket.on("error", (err) => {
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
WorkflowContext,
|
|
13
13
|
StepDefinition,
|
|
14
14
|
TaskStepDefinition,
|
|
15
|
+
LoopStepDefinition,
|
|
16
|
+
PollStepDefinition,
|
|
15
17
|
ParallelStepDefinition,
|
|
16
18
|
ChoiceStepDefinition,
|
|
17
19
|
PassStepDefinition,
|
|
@@ -28,6 +30,8 @@ export interface StateMachineEvents {
|
|
|
28
30
|
onStepCompleted(instanceId: string, stepName: string, output: any, nextStep?: string): void;
|
|
29
31
|
onStepFailed(instanceId: string, stepName: string, error: string, attempts: number): void;
|
|
30
32
|
onStepRetry(instanceId: string, stepName: string, attempt: number, max: number, delayMs: number): void;
|
|
33
|
+
onStepPoll(instanceId: string, stepName: string, pollCount: number, done: boolean, result?: any): void;
|
|
34
|
+
onStepLoop(instanceId: string, stepName: string, loopCount: number, target: string): void;
|
|
31
35
|
onProgress(instanceId: string, progress: number, currentStep: string, completed: number, total: number): void;
|
|
32
36
|
onCompleted(instanceId: string, output: any): void;
|
|
33
37
|
onFailed(instanceId: string, error: string): void;
|
|
@@ -136,11 +140,17 @@ export class WorkflowStateMachine {
|
|
|
136
140
|
this.events.onStepStarted(instanceId, stepName, step.type);
|
|
137
141
|
|
|
138
142
|
// Update step result as running
|
|
143
|
+
const previousStep = freshInstance.stepResults[stepName];
|
|
139
144
|
const stepResult: StepResult = {
|
|
140
145
|
stepName,
|
|
141
146
|
status: "running",
|
|
142
|
-
startedAt: new Date(),
|
|
143
|
-
attempts: (
|
|
147
|
+
startedAt: previousStep?.startedAt ?? new Date(),
|
|
148
|
+
attempts: (previousStep?.attempts ?? 0) + 1,
|
|
149
|
+
pollCount: previousStep?.pollCount,
|
|
150
|
+
lastPolledAt: previousStep?.lastPolledAt,
|
|
151
|
+
loopCount: previousStep?.loopCount,
|
|
152
|
+
lastLoopedAt: previousStep?.lastLoopedAt,
|
|
153
|
+
loopStartedAt: previousStep?.loopStartedAt,
|
|
144
154
|
};
|
|
145
155
|
await this.adapter.updateInstance(instanceId, {
|
|
146
156
|
currentStep: stepName,
|
|
@@ -166,6 +176,12 @@ export class WorkflowStateMachine {
|
|
|
166
176
|
case "pass":
|
|
167
177
|
output = await this.executePassStep(step, ctx);
|
|
168
178
|
break;
|
|
179
|
+
case "poll":
|
|
180
|
+
output = await this.executePollStep(instanceId, step, ctx, definition);
|
|
181
|
+
break;
|
|
182
|
+
case "loop":
|
|
183
|
+
output = await this.executeLoopStep(instanceId, step, ctx);
|
|
184
|
+
break;
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
// Persist step completion
|
|
@@ -176,6 +192,8 @@ export class WorkflowStateMachine {
|
|
|
176
192
|
if (step.type === "choice") {
|
|
177
193
|
// Choice step returns { chosen: "nextStepName" }
|
|
178
194
|
currentStepName = output?.chosen;
|
|
195
|
+
} else if (step.type === "loop" && output?.loopTo) {
|
|
196
|
+
currentStepName = output.loopTo;
|
|
179
197
|
} else if (step.end) {
|
|
180
198
|
currentStepName = undefined;
|
|
181
199
|
} else if (step.next) {
|
|
@@ -445,6 +463,87 @@ export class WorkflowStateMachine {
|
|
|
445
463
|
return output;
|
|
446
464
|
}
|
|
447
465
|
|
|
466
|
+
private async executePollStep(
|
|
467
|
+
instanceId: string,
|
|
468
|
+
step: PollStepDefinition,
|
|
469
|
+
ctx: WorkflowContext,
|
|
470
|
+
_definition: WorkflowDefinition,
|
|
471
|
+
): Promise<any> {
|
|
472
|
+
let input: any;
|
|
473
|
+
|
|
474
|
+
if (step.inputSchema) {
|
|
475
|
+
if (typeof step.inputSchema === "function") {
|
|
476
|
+
input = step.inputSchema(ctx.prev, ctx.input);
|
|
477
|
+
} else {
|
|
478
|
+
const parseResult = step.inputSchema.safeParse(ctx.input);
|
|
479
|
+
if (!parseResult.success) {
|
|
480
|
+
throw new Error(`Input validation failed: ${parseResult.error.message}`);
|
|
481
|
+
}
|
|
482
|
+
input = parseResult.data;
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
input = ctx.input;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let instance = await this.adapter.getInstance(instanceId);
|
|
489
|
+
const stepResult = instance?.stepResults[step.name];
|
|
490
|
+
const startedAt = stepResult?.startedAt ?? new Date();
|
|
491
|
+
|
|
492
|
+
if (instance && stepResult) {
|
|
493
|
+
stepResult.input = stepResult.input ?? input;
|
|
494
|
+
stepResult.pollCount = stepResult.pollCount ?? 0;
|
|
495
|
+
await this.adapter.updateInstance(instanceId, {
|
|
496
|
+
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
while (true) {
|
|
501
|
+
if (step.timeout && Date.now() - startedAt.getTime() > step.timeout) {
|
|
502
|
+
throw new Error(`Poll step "${step.name}" timed out`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
instance = await this.adapter.getInstance(instanceId);
|
|
506
|
+
const sr = instance?.stepResults[step.name];
|
|
507
|
+
const pollCount = sr?.pollCount ?? 0;
|
|
508
|
+
|
|
509
|
+
if (step.maxAttempts && pollCount >= step.maxAttempts) {
|
|
510
|
+
throw new Error(`Poll step "${step.name}" exceeded maxAttempts`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (step.interval > 0) {
|
|
514
|
+
await new Promise((resolve) => setTimeout(resolve, step.interval));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const result = await step.check(input, ctx);
|
|
518
|
+
const nextPollCount = pollCount + 1;
|
|
519
|
+
|
|
520
|
+
if (instance && sr) {
|
|
521
|
+
sr.pollCount = nextPollCount;
|
|
522
|
+
sr.lastPolledAt = new Date();
|
|
523
|
+
sr.output = result;
|
|
524
|
+
await this.adapter.updateInstance(instanceId, {
|
|
525
|
+
stepResults: { ...instance.stepResults, [step.name]: sr },
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this.events.onStepPoll(instanceId, step.name, nextPollCount, result.done, result.result);
|
|
530
|
+
|
|
531
|
+
if (result.done) {
|
|
532
|
+
let output = result.result;
|
|
533
|
+
|
|
534
|
+
if (step.outputSchema) {
|
|
535
|
+
const parseResult = step.outputSchema.safeParse(output);
|
|
536
|
+
if (!parseResult.success) {
|
|
537
|
+
throw new Error(`Output validation failed: ${parseResult.error.message}`);
|
|
538
|
+
}
|
|
539
|
+
output = parseResult.data;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return output;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
448
547
|
private async executePassStep(
|
|
449
548
|
step: PassStepDefinition,
|
|
450
549
|
ctx: WorkflowContext,
|
|
@@ -458,6 +557,52 @@ export class WorkflowStateMachine {
|
|
|
458
557
|
return ctx.input;
|
|
459
558
|
}
|
|
460
559
|
|
|
560
|
+
private async executeLoopStep(
|
|
561
|
+
instanceId: string,
|
|
562
|
+
step: LoopStepDefinition,
|
|
563
|
+
ctx: WorkflowContext,
|
|
564
|
+
): Promise<{ loopTo?: string }> {
|
|
565
|
+
const instance = await this.adapter.getInstance(instanceId);
|
|
566
|
+
const stepResult = instance?.stepResults[step.name] ?? {
|
|
567
|
+
stepName: step.name,
|
|
568
|
+
status: "running" as const,
|
|
569
|
+
attempts: 0,
|
|
570
|
+
startedAt: new Date(),
|
|
571
|
+
};
|
|
572
|
+
const loopStartedAt = stepResult.loopStartedAt ?? stepResult.startedAt ?? new Date();
|
|
573
|
+
const loopCount = stepResult.loopCount ?? 0;
|
|
574
|
+
|
|
575
|
+
if (step.timeout && Date.now() - loopStartedAt.getTime() > step.timeout) {
|
|
576
|
+
throw new Error(`Loop step "${step.name}" timed out`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (step.maxIterations && loopCount >= step.maxIterations) {
|
|
580
|
+
throw new Error(`Loop step "${step.name}" exceeded maxIterations`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const shouldLoop = step.condition(ctx);
|
|
584
|
+
|
|
585
|
+
if (instance) {
|
|
586
|
+
stepResult.loopCount = shouldLoop ? loopCount + 1 : loopCount;
|
|
587
|
+
stepResult.loopStartedAt = loopStartedAt;
|
|
588
|
+
stepResult.lastLoopedAt = shouldLoop ? new Date() : stepResult.lastLoopedAt;
|
|
589
|
+
stepResult.output = { looped: shouldLoop };
|
|
590
|
+
await this.adapter.updateInstance(instanceId, {
|
|
591
|
+
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (shouldLoop) {
|
|
596
|
+
this.events.onStepLoop(instanceId, step.name, loopCount + 1, step.target);
|
|
597
|
+
if (step.interval && step.interval > 0) {
|
|
598
|
+
await new Promise((resolve) => setTimeout(resolve, step.interval));
|
|
599
|
+
}
|
|
600
|
+
return { loopTo: step.target };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return {};
|
|
604
|
+
}
|
|
605
|
+
|
|
461
606
|
// ============================================
|
|
462
607
|
// Context Building
|
|
463
608
|
// ============================================
|