@donkeylabs/server 2.0.27 → 2.0.28
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 +45 -0
- package/package.json +1 -1
- package/src/admin/routes.ts +24 -0
- package/src/core/index.ts +3 -0
- package/src/core/workflow-adapter-kysely.ts +5 -0
- package/src/core/workflow-executor.ts +21 -0
- package/src/core/workflow-socket.ts +7 -0
- package/src/core/workflow-state-machine.ts +147 -2
- package/src/core/workflows.ts +207 -2
package/docs/workflows.md
CHANGED
|
@@ -285,6 +285,51 @@ workflow("example")
|
|
|
285
285
|
.end("done")
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
+
### Poll
|
|
289
|
+
|
|
290
|
+
Use a poll step for wait → check loops that persist across restarts.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
workflow("batch.status")
|
|
294
|
+
.poll("wait-for-result", {
|
|
295
|
+
interval: 5000,
|
|
296
|
+
timeout: 600000,
|
|
297
|
+
maxAttempts: 120,
|
|
298
|
+
check: async (input, ctx) => {
|
|
299
|
+
const status = await fetchStatus(input.operationId);
|
|
300
|
+
if (status.state === "FAILED") throw new Error(status.error);
|
|
301
|
+
if (status.state === "SUCCEEDED") {
|
|
302
|
+
return { done: true, result: status.data };
|
|
303
|
+
}
|
|
304
|
+
return { done: false };
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
.build();
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Each poll cycle emits `workflow.step.poll` events and persists progress to the instance.
|
|
311
|
+
|
|
312
|
+
### Loop
|
|
313
|
+
|
|
314
|
+
Use a loop step to jump back to a previous step until a condition is false.
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
workflow("loop-example")
|
|
318
|
+
.task("increment", {
|
|
319
|
+
handler: async (input) => ({ count: (input.count ?? 0) + 1 }),
|
|
320
|
+
})
|
|
321
|
+
.loop("repeat", {
|
|
322
|
+
condition: (ctx) => ctx.steps.increment.count < 3,
|
|
323
|
+
target: "increment",
|
|
324
|
+
interval: 1000,
|
|
325
|
+
maxIterations: 10,
|
|
326
|
+
timeout: 30000,
|
|
327
|
+
})
|
|
328
|
+
.build();
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Each loop iteration emits `workflow.step.loop` and persists loop counters to the instance.
|
|
332
|
+
|
|
288
333
|
## Workflow Context
|
|
289
334
|
|
|
290
335
|
Every step receives a `WorkflowContext` with:
|
package/package.json
CHANGED
package/src/admin/routes.ts
CHANGED
|
@@ -505,6 +505,17 @@ export function createAdminRouter(config: AdminRouteContext) {
|
|
|
505
505
|
stepName: z.string(),
|
|
506
506
|
error: z.string(),
|
|
507
507
|
}),
|
|
508
|
+
"step.poll": z.object({
|
|
509
|
+
stepName: z.string(),
|
|
510
|
+
pollCount: z.number(),
|
|
511
|
+
done: z.boolean(),
|
|
512
|
+
result: z.any().optional(),
|
|
513
|
+
}),
|
|
514
|
+
"step.loop": z.object({
|
|
515
|
+
stepName: z.string(),
|
|
516
|
+
loopCount: z.number(),
|
|
517
|
+
target: z.string(),
|
|
518
|
+
}),
|
|
508
519
|
completed: z.object({
|
|
509
520
|
output: z.any().optional(),
|
|
510
521
|
}),
|
|
@@ -548,6 +559,19 @@ export function createAdminRouter(config: AdminRouteContext) {
|
|
|
548
559
|
workflowName: z.string(),
|
|
549
560
|
error: z.string(),
|
|
550
561
|
}),
|
|
562
|
+
"workflow.step.poll": z.object({
|
|
563
|
+
instanceId: z.string(),
|
|
564
|
+
stepName: z.string(),
|
|
565
|
+
pollCount: z.number(),
|
|
566
|
+
done: z.boolean(),
|
|
567
|
+
result: z.any().optional(),
|
|
568
|
+
}),
|
|
569
|
+
"workflow.step.loop": z.object({
|
|
570
|
+
instanceId: z.string(),
|
|
571
|
+
stepName: z.string(),
|
|
572
|
+
loopCount: z.number(),
|
|
573
|
+
target: z.string(),
|
|
574
|
+
}),
|
|
551
575
|
},
|
|
552
576
|
handle: (input, ctx) => {
|
|
553
577
|
if (!checkAuth(ctx)) {
|
package/src/core/index.ts
CHANGED
|
@@ -149,6 +149,9 @@ export {
|
|
|
149
149
|
type ChoiceStepDefinition,
|
|
150
150
|
type ChoiceCondition,
|
|
151
151
|
type PassStepDefinition,
|
|
152
|
+
type PollStepDefinition,
|
|
153
|
+
type PollStepResult,
|
|
154
|
+
type LoopStepDefinition,
|
|
152
155
|
type RetryConfig,
|
|
153
156
|
type GetAllWorkflowsOptions,
|
|
154
157
|
type PluginMetadata,
|
|
@@ -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
|
|
|
@@ -184,6 +184,27 @@ function createIpcEventBridge(socket: Socket, instanceId: string): StateMachineE
|
|
|
184
184
|
error,
|
|
185
185
|
});
|
|
186
186
|
},
|
|
187
|
+
onStepPoll: (id, stepName, pollCount, done, result) => {
|
|
188
|
+
sendEvent(socket, {
|
|
189
|
+
type: "step.poll",
|
|
190
|
+
instanceId: id,
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
stepName,
|
|
193
|
+
pollCount,
|
|
194
|
+
done,
|
|
195
|
+
result,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
onStepLoop: (id, stepName, loopCount, target) => {
|
|
199
|
+
sendEvent(socket, {
|
|
200
|
+
type: "step.loop",
|
|
201
|
+
instanceId: id,
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
stepName,
|
|
204
|
+
loopCount,
|
|
205
|
+
target,
|
|
206
|
+
});
|
|
207
|
+
},
|
|
187
208
|
onStepRetry: () => {
|
|
188
209
|
// Retry is internal to the state machine - no IPC event needed
|
|
189
210
|
},
|
|
@@ -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 */
|
|
@@ -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
|
// ============================================
|
package/src/core/workflows.ts
CHANGED
|
@@ -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 });
|
|
@@ -1198,6 +1309,51 @@ class WorkflowsImpl implements Workflows {
|
|
|
1198
1309
|
});
|
|
1199
1310
|
}
|
|
1200
1311
|
},
|
|
1312
|
+
onStepPoll: (id, stepName, pollCount, done, result) => {
|
|
1313
|
+
this.emitEvent("workflow.step.poll", {
|
|
1314
|
+
instanceId: id,
|
|
1315
|
+
stepName,
|
|
1316
|
+
pollCount,
|
|
1317
|
+
done,
|
|
1318
|
+
result,
|
|
1319
|
+
});
|
|
1320
|
+
if (this.sse) {
|
|
1321
|
+
this.sse.broadcast(`workflow:${id}`, "step.poll", {
|
|
1322
|
+
stepName,
|
|
1323
|
+
pollCount,
|
|
1324
|
+
done,
|
|
1325
|
+
result,
|
|
1326
|
+
});
|
|
1327
|
+
this.sse.broadcast("workflows:all", "workflow.step.poll", {
|
|
1328
|
+
instanceId: id,
|
|
1329
|
+
stepName,
|
|
1330
|
+
pollCount,
|
|
1331
|
+
done,
|
|
1332
|
+
result,
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
onStepLoop: (id, stepName, loopCount, target) => {
|
|
1337
|
+
this.emitEvent("workflow.step.loop", {
|
|
1338
|
+
instanceId: id,
|
|
1339
|
+
stepName,
|
|
1340
|
+
loopCount,
|
|
1341
|
+
target,
|
|
1342
|
+
});
|
|
1343
|
+
if (this.sse) {
|
|
1344
|
+
this.sse.broadcast(`workflow:${id}`, "step.loop", {
|
|
1345
|
+
stepName,
|
|
1346
|
+
loopCount,
|
|
1347
|
+
target,
|
|
1348
|
+
});
|
|
1349
|
+
this.sse.broadcast("workflows:all", "workflow.step.loop", {
|
|
1350
|
+
instanceId: id,
|
|
1351
|
+
stepName,
|
|
1352
|
+
loopCount,
|
|
1353
|
+
target,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1201
1357
|
onStepRetry: (id, stepName, attempt, max, delayMs) => {
|
|
1202
1358
|
this.emitEvent("workflow.step.retry", {
|
|
1203
1359
|
instanceId: id,
|
|
@@ -1484,6 +1640,55 @@ class WorkflowsImpl implements Workflows {
|
|
|
1484
1640
|
break;
|
|
1485
1641
|
}
|
|
1486
1642
|
|
|
1643
|
+
case "step.poll": {
|
|
1644
|
+
await this.emitEvent("workflow.step.poll", {
|
|
1645
|
+
instanceId,
|
|
1646
|
+
stepName: event.stepName,
|
|
1647
|
+
pollCount: event.pollCount,
|
|
1648
|
+
done: event.done,
|
|
1649
|
+
result: event.result,
|
|
1650
|
+
});
|
|
1651
|
+
if (this.sse) {
|
|
1652
|
+
this.sse.broadcast(`workflow:${instanceId}`, "step.poll", {
|
|
1653
|
+
stepName: event.stepName,
|
|
1654
|
+
pollCount: event.pollCount,
|
|
1655
|
+
done: event.done,
|
|
1656
|
+
result: event.result,
|
|
1657
|
+
});
|
|
1658
|
+
this.sse.broadcast("workflows:all", "workflow.step.poll", {
|
|
1659
|
+
instanceId,
|
|
1660
|
+
stepName: event.stepName,
|
|
1661
|
+
pollCount: event.pollCount,
|
|
1662
|
+
done: event.done,
|
|
1663
|
+
result: event.result,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
case "step.loop": {
|
|
1670
|
+
await this.emitEvent("workflow.step.loop", {
|
|
1671
|
+
instanceId,
|
|
1672
|
+
stepName: event.stepName,
|
|
1673
|
+
loopCount: event.loopCount,
|
|
1674
|
+
target: event.target,
|
|
1675
|
+
});
|
|
1676
|
+
if (this.sse) {
|
|
1677
|
+
this.sse.broadcast(`workflow:${instanceId}`, "step.loop", {
|
|
1678
|
+
stepName: event.stepName,
|
|
1679
|
+
loopCount: event.loopCount,
|
|
1680
|
+
target: event.target,
|
|
1681
|
+
});
|
|
1682
|
+
this.sse.broadcast("workflows:all", "workflow.step.loop", {
|
|
1683
|
+
instanceId,
|
|
1684
|
+
stepName: event.stepName,
|
|
1685
|
+
loopCount: event.loopCount,
|
|
1686
|
+
target: event.target,
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1487
1692
|
case "progress": {
|
|
1488
1693
|
await this.emitEvent("workflow.progress", {
|
|
1489
1694
|
instanceId,
|