@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.
@@ -59,7 +59,7 @@ type InferZodOutput<T extends ZodSchema> = z.infer<T>;
59
59
  // Step Types
60
60
  // ============================================
61
61
 
62
- export type StepType = "task" | "parallel" | "choice" | "pass";
62
+ export type StepType = "task" | "parallel" | "choice" | "pass" | "poll" | "loop";
63
63
 
64
64
  export interface BaseStepDefinition {
65
65
  name: string;
@@ -146,11 +146,54 @@ export interface PassStepDefinition extends BaseStepDefinition {
146
146
  result?: any;
147
147
  }
148
148
 
149
+ export interface PollStepResult<T = any> {
150
+ done: boolean;
151
+ result?: T;
152
+ }
153
+
154
+ export interface PollStepDefinition<
155
+ TInput extends ZodSchema = ZodSchema,
156
+ TOutput extends ZodSchema = ZodSchema,
157
+ > extends BaseStepDefinition {
158
+ type: "poll";
159
+ /** Wait duration between checks in ms */
160
+ interval: number;
161
+ /** Max total time before failing this step (ms) */
162
+ timeout?: number;
163
+ /** Max number of check cycles before failing */
164
+ maxAttempts?: number;
165
+ /** Input schema or mapper */
166
+ inputSchema?: TInput | ((prev: any, workflowInput: any) => InferZodOutput<TInput>);
167
+ /** Output schema for the final result */
168
+ outputSchema?: TOutput;
169
+ /** Check handler: return done:true to proceed */
170
+ check: (
171
+ input: InferZodOutput<TInput>,
172
+ ctx: WorkflowContext
173
+ ) => Promise<PollStepResult<InferZodOutput<TOutput>>> | PollStepResult<InferZodOutput<TOutput>>;
174
+ }
175
+
176
+ export interface LoopStepDefinition extends BaseStepDefinition {
177
+ type: "loop";
178
+ /** Condition to continue looping */
179
+ condition: (ctx: WorkflowContext) => boolean;
180
+ /** Step name to jump back to when condition is true */
181
+ target: string;
182
+ /** Optional delay before looping (ms) */
183
+ interval?: number;
184
+ /** Max total time before failing this loop (ms) */
185
+ timeout?: number;
186
+ /** Max number of loop iterations before failing */
187
+ maxIterations?: number;
188
+ }
189
+
149
190
  export type StepDefinition =
150
191
  | TaskStepDefinition
151
192
  | ParallelStepDefinition
152
193
  | ChoiceStepDefinition
153
- | PassStepDefinition;
194
+ | PassStepDefinition
195
+ | PollStepDefinition
196
+ | LoopStepDefinition;
154
197
 
155
198
  // ============================================
156
199
  // Workflow Definition
@@ -203,6 +246,11 @@ export interface StepResult {
203
246
  startedAt?: Date;
204
247
  completedAt?: Date;
205
248
  attempts: number;
249
+ pollCount?: number;
250
+ lastPolledAt?: Date;
251
+ loopCount?: number;
252
+ lastLoopedAt?: Date;
253
+ loopStartedAt?: Date;
206
254
  }
207
255
 
208
256
  export interface WorkflowInstance {
@@ -569,6 +617,69 @@ export class WorkflowBuilder {
569
617
  return this;
570
618
  }
571
619
 
620
+ loop(
621
+ name: string,
622
+ config: {
623
+ condition: (ctx: WorkflowContext) => boolean;
624
+ target: string;
625
+ interval?: number;
626
+ timeout?: number;
627
+ maxIterations?: number;
628
+ next?: string;
629
+ end?: boolean;
630
+ }
631
+ ): this {
632
+ const step: LoopStepDefinition = {
633
+ name,
634
+ type: "loop",
635
+ condition: config.condition,
636
+ target: config.target,
637
+ interval: config.interval,
638
+ timeout: config.timeout,
639
+ maxIterations: config.maxIterations,
640
+ next: config.next,
641
+ end: config.end,
642
+ };
643
+
644
+ this.addStep(step);
645
+ return this;
646
+ }
647
+
648
+ poll<TInput extends ZodSchema = ZodSchema, TOutput extends ZodSchema = ZodSchema>(
649
+ name: string,
650
+ config: {
651
+ check: (
652
+ input: InferZodOutput<TInput>,
653
+ ctx: WorkflowContext
654
+ ) => Promise<PollStepResult<InferZodOutput<TOutput>>> | PollStepResult<InferZodOutput<TOutput>>;
655
+ interval: number;
656
+ timeout?: number;
657
+ maxAttempts?: number;
658
+ inputSchema?: TInput | ((prev: any, workflowInput: any) => InferZodOutput<TInput>);
659
+ outputSchema?: TOutput;
660
+ retry?: RetryConfig;
661
+ next?: string;
662
+ end?: boolean;
663
+ }
664
+ ): this {
665
+ const step: PollStepDefinition<TInput, TOutput> = {
666
+ name,
667
+ type: "poll",
668
+ check: config.check,
669
+ interval: config.interval,
670
+ timeout: config.timeout,
671
+ maxAttempts: config.maxAttempts,
672
+ inputSchema: config.inputSchema,
673
+ outputSchema: config.outputSchema,
674
+ retry: config.retry,
675
+ next: config.next,
676
+ end: config.end,
677
+ };
678
+
679
+ this.addStep(step);
680
+ return this;
681
+ }
682
+
572
683
  /** Add an end step (shorthand for pass with end: true) */
573
684
  end(name: string = "end"): this {
574
685
  return this.pass(name, { end: true });
@@ -649,12 +760,22 @@ export interface WorkflowsConfig {
649
760
  heartbeatTimeout?: number;
650
761
  /** Timeout waiting for isolated subprocess readiness (ms, default: 10000) */
651
762
  readyTimeout?: number;
763
+ /** Grace period before SIGKILL when terminating isolated subprocesses (ms, default: 5000) */
764
+ killGraceMs?: number;
765
+ /** SQLite pragmas for isolated subprocess connections */
766
+ sqlitePragmas?: SqlitePragmaConfig;
652
767
  /** Resume strategy for orphaned workflows (default: "blocking") */
653
768
  resumeStrategy?: WorkflowResumeStrategy;
654
769
  }
655
770
 
656
771
  export type WorkflowResumeStrategy = "blocking" | "background" | "skip";
657
772
 
773
+ export interface SqlitePragmaConfig {
774
+ busyTimeout?: number;
775
+ synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
776
+ journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
777
+ }
778
+
658
779
  /** Options for registering a workflow */
659
780
  export interface WorkflowRegisterOptions {
660
781
  /**
@@ -743,6 +864,8 @@ class WorkflowsImpl implements Workflows {
743
864
  private dbPath?: string;
744
865
  private heartbeatTimeoutMs: number;
745
866
  private readyTimeoutMs: number;
867
+ private killGraceMs: number;
868
+ private sqlitePragmas?: SqlitePragmaConfig;
746
869
  private resumeStrategy!: WorkflowResumeStrategy;
747
870
  private workflowModulePaths = new Map<string, string>();
748
871
  private isolatedProcesses = new Map<string, IsolatedProcessInfo>();
@@ -777,6 +900,8 @@ class WorkflowsImpl implements Workflows {
777
900
  this.dbPath = config.dbPath;
778
901
  this.heartbeatTimeoutMs = config.heartbeatTimeout ?? 60000;
779
902
  this.readyTimeoutMs = config.readyTimeout ?? 10000;
903
+ this.killGraceMs = config.killGraceMs ?? 5000;
904
+ this.sqlitePragmas = config.sqlitePragmas;
780
905
  this.resumeStrategy = config.resumeStrategy ?? "blocking";
781
906
  }
782
907
 
@@ -938,11 +1063,7 @@ class WorkflowsImpl implements Workflows {
938
1063
  // Kill isolated process if running
939
1064
  const isolatedInfo = this.isolatedProcesses.get(instanceId);
940
1065
  if (isolatedInfo) {
941
- try {
942
- process.kill(isolatedInfo.pid, "SIGTERM");
943
- } catch {
944
- // Process might already be dead
945
- }
1066
+ await killProcessWithGrace(isolatedInfo.pid, this.killGraceMs);
946
1067
  if (isolatedInfo.timeout) clearTimeout(isolatedInfo.timeout);
947
1068
  if (isolatedInfo.heartbeatTimeout) clearTimeout(isolatedInfo.heartbeatTimeout);
948
1069
  this.isolatedProcesses.delete(instanceId);
@@ -1198,6 +1319,51 @@ class WorkflowsImpl implements Workflows {
1198
1319
  });
1199
1320
  }
1200
1321
  },
1322
+ onStepPoll: (id, stepName, pollCount, done, result) => {
1323
+ this.emitEvent("workflow.step.poll", {
1324
+ instanceId: id,
1325
+ stepName,
1326
+ pollCount,
1327
+ done,
1328
+ result,
1329
+ });
1330
+ if (this.sse) {
1331
+ this.sse.broadcast(`workflow:${id}`, "step.poll", {
1332
+ stepName,
1333
+ pollCount,
1334
+ done,
1335
+ result,
1336
+ });
1337
+ this.sse.broadcast("workflows:all", "workflow.step.poll", {
1338
+ instanceId: id,
1339
+ stepName,
1340
+ pollCount,
1341
+ done,
1342
+ result,
1343
+ });
1344
+ }
1345
+ },
1346
+ onStepLoop: (id, stepName, loopCount, target) => {
1347
+ this.emitEvent("workflow.step.loop", {
1348
+ instanceId: id,
1349
+ stepName,
1350
+ loopCount,
1351
+ target,
1352
+ });
1353
+ if (this.sse) {
1354
+ this.sse.broadcast(`workflow:${id}`, "step.loop", {
1355
+ stepName,
1356
+ loopCount,
1357
+ target,
1358
+ });
1359
+ this.sse.broadcast("workflows:all", "workflow.step.loop", {
1360
+ instanceId: id,
1361
+ stepName,
1362
+ loopCount,
1363
+ target,
1364
+ });
1365
+ }
1366
+ },
1201
1367
  onStepRetry: (id, stepName, attempt, max, delayMs) => {
1202
1368
  this.emitEvent("workflow.step.retry", {
1203
1369
  instanceId: id,
@@ -1314,6 +1480,7 @@ class WorkflowsImpl implements Workflows {
1314
1480
  pluginModulePaths: this.pluginModulePaths,
1315
1481
  pluginConfigs,
1316
1482
  coreConfig,
1483
+ sqlitePragmas: this.sqlitePragmas,
1317
1484
  };
1318
1485
 
1319
1486
  // Spawn the subprocess
@@ -1484,6 +1651,55 @@ class WorkflowsImpl implements Workflows {
1484
1651
  break;
1485
1652
  }
1486
1653
 
1654
+ case "step.poll": {
1655
+ await this.emitEvent("workflow.step.poll", {
1656
+ instanceId,
1657
+ stepName: event.stepName,
1658
+ pollCount: event.pollCount,
1659
+ done: event.done,
1660
+ result: event.result,
1661
+ });
1662
+ if (this.sse) {
1663
+ this.sse.broadcast(`workflow:${instanceId}`, "step.poll", {
1664
+ stepName: event.stepName,
1665
+ pollCount: event.pollCount,
1666
+ done: event.done,
1667
+ result: event.result,
1668
+ });
1669
+ this.sse.broadcast("workflows:all", "workflow.step.poll", {
1670
+ instanceId,
1671
+ stepName: event.stepName,
1672
+ pollCount: event.pollCount,
1673
+ done: event.done,
1674
+ result: event.result,
1675
+ });
1676
+ }
1677
+ break;
1678
+ }
1679
+
1680
+ case "step.loop": {
1681
+ await this.emitEvent("workflow.step.loop", {
1682
+ instanceId,
1683
+ stepName: event.stepName,
1684
+ loopCount: event.loopCount,
1685
+ target: event.target,
1686
+ });
1687
+ if (this.sse) {
1688
+ this.sse.broadcast(`workflow:${instanceId}`, "step.loop", {
1689
+ stepName: event.stepName,
1690
+ loopCount: event.loopCount,
1691
+ target: event.target,
1692
+ });
1693
+ this.sse.broadcast("workflows:all", "workflow.step.loop", {
1694
+ instanceId,
1695
+ stepName: event.stepName,
1696
+ loopCount: event.loopCount,
1697
+ target: event.target,
1698
+ });
1699
+ }
1700
+ break;
1701
+ }
1702
+
1487
1703
  case "progress": {
1488
1704
  await this.emitEvent("workflow.progress", {
1489
1705
  instanceId,
@@ -1790,6 +2006,11 @@ class WorkflowsImpl implements Workflows {
1790
2006
  }
1791
2007
 
1792
2008
  console.error(`[Workflows] No heartbeat from isolated workflow ${instanceId} for ${this.heartbeatTimeoutMs}ms`);
2009
+ await this.emitEvent("workflow.watchdog.stale", {
2010
+ instanceId,
2011
+ reason: "heartbeat",
2012
+ timeoutMs: this.heartbeatTimeoutMs,
2013
+ });
1793
2014
  await this.handleIsolatedTimeout(instanceId, pid);
1794
2015
  }, this.heartbeatTimeoutMs);
1795
2016
  }
@@ -1801,12 +2022,12 @@ class WorkflowsImpl implements Workflows {
1801
2022
  const info = this.isolatedProcesses.get(instanceId);
1802
2023
  if (!info) return;
1803
2024
 
1804
- // Kill the process
1805
- try {
1806
- process.kill(pid, "SIGKILL");
1807
- } catch {
1808
- // Process might already be dead
1809
- }
2025
+ await killProcessWithGrace(pid, this.killGraceMs);
2026
+ await this.emitEvent("workflow.watchdog.killed", {
2027
+ instanceId,
2028
+ reason: "timeout",
2029
+ timeoutMs: this.heartbeatTimeoutMs,
2030
+ });
1810
2031
 
1811
2032
  // Clean up
1812
2033
  if (info.timeout) clearTimeout(info.timeout);
@@ -1943,3 +2164,29 @@ function isPlainObject(value: Record<string, any>): boolean {
1943
2164
  export function createWorkflows(config?: WorkflowsConfig): Workflows {
1944
2165
  return new WorkflowsImpl(config);
1945
2166
  }
2167
+
2168
+ async function killProcessWithGrace(pid: number, graceMs: number): Promise<void> {
2169
+ try {
2170
+ process.kill(pid, "SIGTERM");
2171
+ } catch {
2172
+ return;
2173
+ }
2174
+
2175
+ if (graceMs <= 0) {
2176
+ try {
2177
+ process.kill(pid, "SIGKILL");
2178
+ } catch {
2179
+ return;
2180
+ }
2181
+ return;
2182
+ }
2183
+
2184
+ await new Promise((resolve) => setTimeout(resolve, graceMs));
2185
+
2186
+ try {
2187
+ process.kill(pid, 0);
2188
+ process.kill(pid, "SIGKILL");
2189
+ } catch {
2190
+ // Process already exited
2191
+ }
2192
+ }