@mastra/inngest 0.0.0-vnext-inngest-20250506123700
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/CHANGELOG.md +21 -0
- package/LICENSE.md +46 -0
- package/dist/_tsup-dts-rollup.d.cts +183 -0
- package/dist/_tsup-dts-rollup.d.ts +183 -0
- package/dist/index.cjs +693 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +687 -0
- package/docker-compose.yaml +10 -0
- package/package.json +50 -0
- package/src/index.test.ts +5338 -0
- package/src/index.ts +898 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +8 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NewWorkflow,
|
|
3
|
+
type NewStep as Step,
|
|
4
|
+
DefaultExecutionEngine,
|
|
5
|
+
Run,
|
|
6
|
+
cloneStep,
|
|
7
|
+
} from '@mastra/core/workflows/vNext';
|
|
8
|
+
import type {
|
|
9
|
+
ExecuteFunction,
|
|
10
|
+
ExecutionContext,
|
|
11
|
+
ExecutionEngine,
|
|
12
|
+
ExecutionGraph,
|
|
13
|
+
NewStep,
|
|
14
|
+
NewWorkflowConfig,
|
|
15
|
+
StepFlowEntry,
|
|
16
|
+
StepResult,
|
|
17
|
+
WorkflowResult,
|
|
18
|
+
} from '@mastra/core/workflows/vNext';
|
|
19
|
+
import { type Span } from '@opentelemetry/api';
|
|
20
|
+
import { Inngest, type BaseContext } from 'inngest';
|
|
21
|
+
import { subscribe } from '@inngest/realtime';
|
|
22
|
+
import { serve as inngestServe } from 'inngest/hono';
|
|
23
|
+
import { type Mastra } from '@mastra/core';
|
|
24
|
+
import type { z } from 'zod';
|
|
25
|
+
import { RuntimeContext } from '@mastra/core/di';
|
|
26
|
+
import { randomUUID } from 'crypto';
|
|
27
|
+
|
|
28
|
+
import { createStep } from '@mastra/core/workflows/vNext';
|
|
29
|
+
|
|
30
|
+
export function serve({ mastra, ingest }: { mastra: Mastra; ingest: Inngest }): ReturnType<typeof inngestServe> {
|
|
31
|
+
const wfs = mastra.vnext_getWorkflows();
|
|
32
|
+
const functions = Object.values(wfs).flatMap(wf => {
|
|
33
|
+
if (wf instanceof InngestWorkflow) {
|
|
34
|
+
return wf.getFunctions();
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
});
|
|
38
|
+
return inngestServe({
|
|
39
|
+
client: ingest,
|
|
40
|
+
functions,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class InngestRun<
|
|
45
|
+
TSteps extends NewStep<string, any, any>[] = NewStep<string, any, any>[],
|
|
46
|
+
TInput extends z.ZodType<any> = z.ZodType<any>,
|
|
47
|
+
TOutput extends z.ZodType<any> = z.ZodType<any>,
|
|
48
|
+
> extends Run<TSteps, TInput, TOutput> {
|
|
49
|
+
private inngest: Inngest;
|
|
50
|
+
#mastra: Mastra;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
params: {
|
|
54
|
+
workflowId: string;
|
|
55
|
+
runId: string;
|
|
56
|
+
executionEngine: ExecutionEngine;
|
|
57
|
+
executionGraph: ExecutionGraph;
|
|
58
|
+
mastra?: Mastra;
|
|
59
|
+
retryConfig?: {
|
|
60
|
+
attempts?: number;
|
|
61
|
+
delay?: number;
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
inngest: Inngest,
|
|
65
|
+
) {
|
|
66
|
+
super(params);
|
|
67
|
+
this.inngest = inngest;
|
|
68
|
+
this.#mastra = params.mastra!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getRuns(eventId: string) {
|
|
72
|
+
const response = await fetch(`${this.inngest.apiBaseUrl}/v1/events/${eventId}/runs`, {
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${process.env.INNGEST_SIGNING_KEY}`,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const json = await response.json();
|
|
78
|
+
return (json as any).data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getRunOutput(eventId: string) {
|
|
82
|
+
let runs = await this.getRuns(eventId);
|
|
83
|
+
while (runs?.[0]?.status !== 'Completed') {
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
85
|
+
runs = await this.getRuns(eventId);
|
|
86
|
+
if (runs?.[0]?.status === 'Failed' || runs?.[0]?.status === 'Cancelled') {
|
|
87
|
+
throw new Error(`Function run ${runs?.[0]?.status}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return runs?.[0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async start({
|
|
94
|
+
inputData,
|
|
95
|
+
runtimeContext,
|
|
96
|
+
}: {
|
|
97
|
+
inputData?: z.infer<TInput>;
|
|
98
|
+
runtimeContext?: RuntimeContext;
|
|
99
|
+
}): Promise<WorkflowResult<TOutput, TSteps>> {
|
|
100
|
+
const eventOutput = await this.inngest.send({
|
|
101
|
+
name: `workflow.${this.workflowId}`,
|
|
102
|
+
data: {
|
|
103
|
+
inputData,
|
|
104
|
+
runId: this.runId,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const eventId = eventOutput.ids[0];
|
|
109
|
+
if (!eventId) {
|
|
110
|
+
throw new Error('Event ID is not set');
|
|
111
|
+
}
|
|
112
|
+
const runOutput = await this.getRunOutput(eventId);
|
|
113
|
+
const result = runOutput?.output?.result;
|
|
114
|
+
if (result.status === 'failed') {
|
|
115
|
+
result.error = new Error(result.error);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async resume<TResumeSchema extends z.ZodType<any>>(params: {
|
|
121
|
+
resumeData?: z.infer<TResumeSchema>;
|
|
122
|
+
step:
|
|
123
|
+
| Step<string, any, any, TResumeSchema, any>
|
|
124
|
+
| [...Step<string, any, any, any, any>[], Step<string, any, any, TResumeSchema, any>]
|
|
125
|
+
| string
|
|
126
|
+
| string[];
|
|
127
|
+
runtimeContext?: RuntimeContext;
|
|
128
|
+
}): Promise<WorkflowResult<TOutput, TSteps>> {
|
|
129
|
+
const steps: string[] = (Array.isArray(params.step) ? params.step : [params.step]).map(step =>
|
|
130
|
+
typeof step === 'string' ? step : step?.id,
|
|
131
|
+
);
|
|
132
|
+
const snapshot = await this.#mastra?.storage?.loadWorkflowSnapshot({
|
|
133
|
+
workflowName: this.workflowId,
|
|
134
|
+
runId: this.runId,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const eventOutput = await this.inngest.send({
|
|
138
|
+
name: `workflow.${this.workflowId}`,
|
|
139
|
+
data: {
|
|
140
|
+
inputData: params.resumeData,
|
|
141
|
+
runId: this.runId,
|
|
142
|
+
stepResults: snapshot?.context as any,
|
|
143
|
+
resume: {
|
|
144
|
+
steps,
|
|
145
|
+
stepResults: snapshot?.context as any,
|
|
146
|
+
resumePayload: params.resumeData,
|
|
147
|
+
// @ts-ignore
|
|
148
|
+
resumePath: snapshot?.suspendedPaths?.[steps?.[0]] as any,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const eventId = eventOutput.ids[0];
|
|
154
|
+
if (!eventId) {
|
|
155
|
+
throw new Error('Event ID is not set');
|
|
156
|
+
}
|
|
157
|
+
const runOutput = await this.getRunOutput(eventId);
|
|
158
|
+
const result = runOutput?.output?.result;
|
|
159
|
+
if (result.status === 'failed') {
|
|
160
|
+
result.error = new Error(result.error);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
watch(cb: (event: any) => void): () => void {
|
|
166
|
+
const streamPromise = subscribe(
|
|
167
|
+
{
|
|
168
|
+
channel: `workflow:${this.workflowId}:${this.runId}`,
|
|
169
|
+
topics: ['watch'],
|
|
170
|
+
app: this.inngest,
|
|
171
|
+
},
|
|
172
|
+
(message: any) => {
|
|
173
|
+
cb(message.data);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
streamPromise.then((stream: any) => {
|
|
179
|
+
stream.cancel();
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class InngestWorkflow<
|
|
186
|
+
TSteps extends NewStep<string, any, any>[] = NewStep<string, any, any>[],
|
|
187
|
+
TWorkflowId extends string = string,
|
|
188
|
+
TInput extends z.ZodType<any> = z.ZodType<any>,
|
|
189
|
+
TOutput extends z.ZodType<any> = z.ZodType<any>,
|
|
190
|
+
TPrevSchema extends z.ZodType<any> = TInput,
|
|
191
|
+
> extends NewWorkflow<TSteps, TWorkflowId, TInput, TOutput, TPrevSchema> {
|
|
192
|
+
#mastra: Mastra;
|
|
193
|
+
public inngest: Inngest;
|
|
194
|
+
|
|
195
|
+
private function: ReturnType<Inngest['createFunction']> | undefined;
|
|
196
|
+
|
|
197
|
+
constructor(params: NewWorkflowConfig<TWorkflowId, TInput, TOutput, TSteps>, inngest: Inngest) {
|
|
198
|
+
super(params);
|
|
199
|
+
this.#mastra = params.mastra!;
|
|
200
|
+
this.inngest = inngest;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
__registerMastra(mastra: Mastra) {
|
|
204
|
+
this.#mastra = mastra;
|
|
205
|
+
this.executionEngine.__registerMastra(mastra);
|
|
206
|
+
|
|
207
|
+
if (this.executionGraph.steps.length) {
|
|
208
|
+
for (const step of this.executionGraph.steps) {
|
|
209
|
+
if (step.type === 'step' && step.step instanceof InngestWorkflow) {
|
|
210
|
+
step.step.__registerMastra(mastra);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
createRun(options?: { runId?: string }): Run<TSteps, TInput, TOutput> {
|
|
217
|
+
const runIdToUse = options?.runId || randomUUID();
|
|
218
|
+
|
|
219
|
+
// Return a new Run instance with object parameters
|
|
220
|
+
return new InngestRun(
|
|
221
|
+
{
|
|
222
|
+
workflowId: this.id,
|
|
223
|
+
runId: runIdToUse,
|
|
224
|
+
executionEngine: this.executionEngine,
|
|
225
|
+
executionGraph: this.executionGraph,
|
|
226
|
+
mastra: this.#mastra,
|
|
227
|
+
retryConfig: this.retryConfig,
|
|
228
|
+
},
|
|
229
|
+
this.inngest,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getFunction() {
|
|
234
|
+
if (this.function) {
|
|
235
|
+
return this.function;
|
|
236
|
+
}
|
|
237
|
+
this.function = this.inngest.createFunction(
|
|
238
|
+
// @ts-ignore
|
|
239
|
+
{ id: `workflow.${this.id}`, retries: this.retryConfig?.attempts ?? 0 },
|
|
240
|
+
{ event: `workflow.${this.id}` },
|
|
241
|
+
async ({ event, step, attempt, publish }) => {
|
|
242
|
+
let { inputData, runId, resume } = event.data;
|
|
243
|
+
|
|
244
|
+
if (!runId) {
|
|
245
|
+
runId = await step.run(`workflow.${this.id}.runIdGen`, async () => {
|
|
246
|
+
return randomUUID();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const emitter = {
|
|
251
|
+
emit: async (event: string, data: any) => {
|
|
252
|
+
if (!publish) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await publish({
|
|
258
|
+
channel: `workflow:${this.id}:${runId}`,
|
|
259
|
+
topic: 'watch',
|
|
260
|
+
data,
|
|
261
|
+
});
|
|
262
|
+
} catch (err: any) {
|
|
263
|
+
this.logger.error('Error emitting event: ' + (err?.stack ?? err?.message ?? err));
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const engine = new InngestExecutionEngine(this.#mastra, step, attempt);
|
|
269
|
+
const result = await engine.execute<z.infer<TInput>, WorkflowResult<TOutput, TSteps>>({
|
|
270
|
+
workflowId: this.id,
|
|
271
|
+
runId,
|
|
272
|
+
graph: this.executionGraph,
|
|
273
|
+
input: inputData,
|
|
274
|
+
emitter,
|
|
275
|
+
retryConfig: this.retryConfig,
|
|
276
|
+
runtimeContext: new RuntimeContext(), // TODO
|
|
277
|
+
resume,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return { result, runId };
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
return this.function;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getNestedFunctions(steps: StepFlowEntry[]): ReturnType<Inngest['createFunction']>[] {
|
|
287
|
+
return steps.flatMap(step => {
|
|
288
|
+
if (step.type === 'step' || step.type === 'loop' || step.type === 'foreach') {
|
|
289
|
+
if (step.step instanceof InngestWorkflow) {
|
|
290
|
+
return [step.step.getFunction(), ...step.step.getNestedFunctions(step.step.executionGraph.steps)];
|
|
291
|
+
}
|
|
292
|
+
return [];
|
|
293
|
+
} else if (step.type === 'parallel' || step.type === 'conditional') {
|
|
294
|
+
return this.getNestedFunctions(step.steps);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return [];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getFunctions() {
|
|
302
|
+
return [this.getFunction(), ...this.getNestedFunctions(this.executionGraph.steps)];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function cloneWorkflow<
|
|
307
|
+
TWorkflowId extends string = string,
|
|
308
|
+
TInput extends z.ZodType<any> = z.ZodType<any>,
|
|
309
|
+
TOutput extends z.ZodType<any> = z.ZodType<any>,
|
|
310
|
+
TSteps extends Step<string, any, any, any, any>[] = Step<string, any, any, any, any>[],
|
|
311
|
+
>(
|
|
312
|
+
workflow: InngestWorkflow<TSteps, string, TInput, TOutput>,
|
|
313
|
+
opts: { id: TWorkflowId },
|
|
314
|
+
): InngestWorkflow<TSteps, TWorkflowId, TInput, TOutput> {
|
|
315
|
+
const wf = new InngestWorkflow(
|
|
316
|
+
{
|
|
317
|
+
id: opts.id,
|
|
318
|
+
inputSchema: workflow.inputSchema,
|
|
319
|
+
outputSchema: workflow.outputSchema,
|
|
320
|
+
steps: workflow.stepDefs,
|
|
321
|
+
mastra: workflow.mastra,
|
|
322
|
+
},
|
|
323
|
+
workflow.inngest,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
wf.setStepFlow(workflow.stepGraph);
|
|
327
|
+
wf.commit();
|
|
328
|
+
return wf;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function init(inngest: Inngest) {
|
|
332
|
+
return {
|
|
333
|
+
createWorkflow<
|
|
334
|
+
TWorkflowId extends string = string,
|
|
335
|
+
TInput extends z.ZodType<any> = z.ZodType<any>,
|
|
336
|
+
TOutput extends z.ZodType<any> = z.ZodType<any>,
|
|
337
|
+
TSteps extends Step<string, any, any>[] = Step<string, any, any>[],
|
|
338
|
+
>(params: NewWorkflowConfig<TWorkflowId, TInput, TOutput, TSteps>) {
|
|
339
|
+
return new InngestWorkflow(params, inngest);
|
|
340
|
+
},
|
|
341
|
+
createStep,
|
|
342
|
+
cloneStep,
|
|
343
|
+
cloneWorkflow,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export class InngestExecutionEngine extends DefaultExecutionEngine {
|
|
348
|
+
private inngestStep: BaseContext<Inngest>['step'];
|
|
349
|
+
private inngestAttempts: number;
|
|
350
|
+
|
|
351
|
+
constructor(mastra: Mastra, inngestStep: BaseContext<Inngest>['step'], inngestAttempts: number = 0) {
|
|
352
|
+
super({ mastra });
|
|
353
|
+
this.inngestStep = inngestStep;
|
|
354
|
+
this.inngestAttempts = inngestAttempts;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected async fmtReturnValue<TOutput>(
|
|
358
|
+
executionSpan: Span | undefined,
|
|
359
|
+
emitter: { emit: (event: string, data: any) => Promise<void> },
|
|
360
|
+
stepResults: Record<string, StepResult<any>>,
|
|
361
|
+
lastOutput: StepResult<any>,
|
|
362
|
+
error?: Error | string,
|
|
363
|
+
): Promise<TOutput> {
|
|
364
|
+
const base: any = {
|
|
365
|
+
status: lastOutput.status,
|
|
366
|
+
steps: stepResults,
|
|
367
|
+
};
|
|
368
|
+
if (lastOutput.status === 'success') {
|
|
369
|
+
await emitter.emit('watch', {
|
|
370
|
+
type: 'watch',
|
|
371
|
+
payload: {
|
|
372
|
+
workflowState: {
|
|
373
|
+
status: lastOutput.status,
|
|
374
|
+
steps: stepResults,
|
|
375
|
+
result: lastOutput.output,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
eventTimestamp: Date.now(),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
base.result = lastOutput.output;
|
|
382
|
+
} else if (lastOutput.status === 'failed') {
|
|
383
|
+
base.error =
|
|
384
|
+
error instanceof Error
|
|
385
|
+
? (error?.stack ?? error.message)
|
|
386
|
+
: lastOutput?.error instanceof Error
|
|
387
|
+
? lastOutput.error.message
|
|
388
|
+
: (lastOutput.error ?? error ?? 'Unknown error');
|
|
389
|
+
|
|
390
|
+
await emitter.emit('watch', {
|
|
391
|
+
type: 'watch',
|
|
392
|
+
payload: {
|
|
393
|
+
workflowState: {
|
|
394
|
+
status: lastOutput.status,
|
|
395
|
+
steps: stepResults,
|
|
396
|
+
result: null,
|
|
397
|
+
error: base.error,
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
eventTimestamp: Date.now(),
|
|
401
|
+
});
|
|
402
|
+
} else if (lastOutput.status === 'suspended') {
|
|
403
|
+
await emitter.emit('watch', {
|
|
404
|
+
type: 'watch',
|
|
405
|
+
payload: {
|
|
406
|
+
workflowState: {
|
|
407
|
+
status: lastOutput.status,
|
|
408
|
+
steps: stepResults,
|
|
409
|
+
result: null,
|
|
410
|
+
error: null,
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
eventTimestamp: Date.now(),
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const suspendedStepIds = Object.entries(stepResults).flatMap(([stepId, stepResult]) => {
|
|
417
|
+
if (stepResult?.status === 'suspended') {
|
|
418
|
+
const nestedPath = stepResult?.payload?.__workflow_meta?.path;
|
|
419
|
+
return nestedPath ? [[stepId, ...nestedPath]] : [[stepId]];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return [];
|
|
423
|
+
});
|
|
424
|
+
base.suspended = suspendedStepIds;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
executionSpan?.end();
|
|
428
|
+
return base as TOutput;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async superExecuteStep({
|
|
432
|
+
workflowId,
|
|
433
|
+
runId,
|
|
434
|
+
step,
|
|
435
|
+
stepResults,
|
|
436
|
+
executionContext,
|
|
437
|
+
resume,
|
|
438
|
+
prevOutput,
|
|
439
|
+
emitter,
|
|
440
|
+
runtimeContext,
|
|
441
|
+
}: {
|
|
442
|
+
workflowId: string;
|
|
443
|
+
runId: string;
|
|
444
|
+
step: Step<string, any, any>;
|
|
445
|
+
stepResults: Record<string, StepResult<any>>;
|
|
446
|
+
executionContext: ExecutionContext;
|
|
447
|
+
resume?: {
|
|
448
|
+
steps: string[];
|
|
449
|
+
resumePayload: any;
|
|
450
|
+
};
|
|
451
|
+
prevOutput: any;
|
|
452
|
+
emitter: { emit: (event: string, data: any) => Promise<void> };
|
|
453
|
+
runtimeContext: RuntimeContext;
|
|
454
|
+
}): Promise<StepResult<any>> {
|
|
455
|
+
return super.executeStep({
|
|
456
|
+
workflowId,
|
|
457
|
+
runId,
|
|
458
|
+
step,
|
|
459
|
+
stepResults,
|
|
460
|
+
executionContext,
|
|
461
|
+
resume,
|
|
462
|
+
prevOutput,
|
|
463
|
+
emitter,
|
|
464
|
+
runtimeContext,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async executeStep({
|
|
469
|
+
step,
|
|
470
|
+
stepResults,
|
|
471
|
+
executionContext,
|
|
472
|
+
resume,
|
|
473
|
+
prevOutput,
|
|
474
|
+
emitter,
|
|
475
|
+
runtimeContext,
|
|
476
|
+
}: {
|
|
477
|
+
step: Step<string, any, any>;
|
|
478
|
+
stepResults: Record<string, StepResult<any>>;
|
|
479
|
+
executionContext: {
|
|
480
|
+
workflowId: string;
|
|
481
|
+
runId: string;
|
|
482
|
+
executionPath: number[];
|
|
483
|
+
suspendedPaths: Record<string, number[]>;
|
|
484
|
+
retryConfig: { attempts: number; delay: number };
|
|
485
|
+
};
|
|
486
|
+
resume?: {
|
|
487
|
+
steps: string[];
|
|
488
|
+
resumePayload: any;
|
|
489
|
+
runId?: string;
|
|
490
|
+
};
|
|
491
|
+
prevOutput: any;
|
|
492
|
+
emitter: { emit: (event: string, data: any) => Promise<void> };
|
|
493
|
+
runtimeContext: RuntimeContext;
|
|
494
|
+
}): Promise<StepResult<any>> {
|
|
495
|
+
await this.inngestStep.run(
|
|
496
|
+
`workflow.${executionContext.workflowId}.run.${executionContext.runId}.step.${step.id}.running_ev`,
|
|
497
|
+
async () => {
|
|
498
|
+
await emitter.emit('watch', {
|
|
499
|
+
type: 'watch',
|
|
500
|
+
payload: {
|
|
501
|
+
currentStep: {
|
|
502
|
+
id: step.id,
|
|
503
|
+
status: 'running',
|
|
504
|
+
},
|
|
505
|
+
workflowState: {
|
|
506
|
+
status: 'running',
|
|
507
|
+
steps: {
|
|
508
|
+
...stepResults,
|
|
509
|
+
[step.id]: {
|
|
510
|
+
status: 'running',
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
result: null,
|
|
514
|
+
error: null,
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
eventTimestamp: Date.now(),
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
if (step instanceof InngestWorkflow) {
|
|
523
|
+
const isResume = !!resume?.steps?.length;
|
|
524
|
+
let result: WorkflowResult<any, any>;
|
|
525
|
+
let runId: string;
|
|
526
|
+
if (isResume) {
|
|
527
|
+
// @ts-ignore
|
|
528
|
+
runId = stepResults[resume?.steps?.[0]]?.payload?.__workflow_meta?.runId ?? randomUUID();
|
|
529
|
+
|
|
530
|
+
const snapshot: any = await this.mastra?.getStorage()?.loadWorkflowSnapshot({
|
|
531
|
+
workflowName: step.id,
|
|
532
|
+
runId: runId,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const invokeResp = (await this.inngestStep.invoke(`workflow.${executionContext.workflowId}.step.${step.id}`, {
|
|
536
|
+
function: step.getFunction(),
|
|
537
|
+
data: {
|
|
538
|
+
inputData: prevOutput,
|
|
539
|
+
runId: runId,
|
|
540
|
+
resume: {
|
|
541
|
+
runId: runId,
|
|
542
|
+
steps: resume.steps.slice(1),
|
|
543
|
+
stepResults: snapshot?.context as any,
|
|
544
|
+
resumePayload: resume.resumePayload,
|
|
545
|
+
// @ts-ignore
|
|
546
|
+
resumePath: snapshot?.suspendedPaths?.[resume.steps?.[1]] as any,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
})) as any;
|
|
550
|
+
result = invokeResp.result;
|
|
551
|
+
runId = invokeResp.runId;
|
|
552
|
+
} else {
|
|
553
|
+
const invokeResp = (await this.inngestStep.invoke(`workflow.${executionContext.workflowId}.step.${step.id}`, {
|
|
554
|
+
function: step.getFunction(),
|
|
555
|
+
data: {
|
|
556
|
+
inputData: prevOutput,
|
|
557
|
+
},
|
|
558
|
+
})) as any;
|
|
559
|
+
result = invokeResp.result;
|
|
560
|
+
runId = invokeResp.runId;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const res = await this.inngestStep.run(
|
|
564
|
+
`workflow.${executionContext.workflowId}.step.${step.id}.nestedwf-results`,
|
|
565
|
+
async () => {
|
|
566
|
+
if (result.status === 'failed') {
|
|
567
|
+
await emitter.emit('watch', {
|
|
568
|
+
type: 'watch',
|
|
569
|
+
payload: {
|
|
570
|
+
currentStep: {
|
|
571
|
+
id: step.id,
|
|
572
|
+
status: 'failed',
|
|
573
|
+
error: result?.error,
|
|
574
|
+
},
|
|
575
|
+
workflowState: {
|
|
576
|
+
status: 'running',
|
|
577
|
+
steps: stepResults,
|
|
578
|
+
result: null,
|
|
579
|
+
error: null,
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
eventTimestamp: Date.now(),
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
return { executionContext, result: { status: 'failed', error: result?.error } };
|
|
586
|
+
} else if (result.status === 'suspended') {
|
|
587
|
+
const suspendedSteps = Object.entries(result.steps).filter(([_stepName, stepResult]) => {
|
|
588
|
+
const stepRes: StepResult<any> = stepResult as StepResult<any>;
|
|
589
|
+
return stepRes?.status === 'suspended';
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
for (const [stepName, stepResult] of suspendedSteps) {
|
|
593
|
+
// @ts-ignore
|
|
594
|
+
const suspendPath: string[] = [stepName, ...(stepResult?.payload?.__workflow_meta?.path ?? [])];
|
|
595
|
+
executionContext.suspendedPaths[step.id] = executionContext.executionPath;
|
|
596
|
+
|
|
597
|
+
await emitter.emit('watch', {
|
|
598
|
+
type: 'watch',
|
|
599
|
+
payload: {
|
|
600
|
+
currentStep: {
|
|
601
|
+
id: step.id,
|
|
602
|
+
status: 'suspended',
|
|
603
|
+
payload: { ...(stepResult as any)?.payload, __workflow_meta: { runId: runId, path: suspendPath } },
|
|
604
|
+
},
|
|
605
|
+
workflowState: {
|
|
606
|
+
status: 'running',
|
|
607
|
+
steps: stepResults,
|
|
608
|
+
result: null,
|
|
609
|
+
error: null,
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
eventTimestamp: Date.now(),
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
executionContext,
|
|
617
|
+
result: {
|
|
618
|
+
status: 'suspended',
|
|
619
|
+
payload: { ...(stepResult as any)?.payload, __workflow_meta: { runId: runId, path: suspendPath } },
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
await emitter.emit('watch', {
|
|
625
|
+
type: 'watch',
|
|
626
|
+
payload: {
|
|
627
|
+
currentStep: {
|
|
628
|
+
id: step.id,
|
|
629
|
+
status: 'suspended',
|
|
630
|
+
payload: {},
|
|
631
|
+
},
|
|
632
|
+
workflowState: {
|
|
633
|
+
status: 'running',
|
|
634
|
+
steps: stepResults,
|
|
635
|
+
result: null,
|
|
636
|
+
error: null,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
eventTimestamp: Date.now(),
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
executionContext,
|
|
644
|
+
result: {
|
|
645
|
+
status: 'suspended',
|
|
646
|
+
payload: {},
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// is success
|
|
652
|
+
|
|
653
|
+
await emitter.emit('watch', {
|
|
654
|
+
type: 'watch',
|
|
655
|
+
payload: {
|
|
656
|
+
currentStep: {
|
|
657
|
+
id: step.id,
|
|
658
|
+
status: 'success',
|
|
659
|
+
output: result?.result,
|
|
660
|
+
},
|
|
661
|
+
workflowState: {
|
|
662
|
+
status: 'running',
|
|
663
|
+
steps: stepResults,
|
|
664
|
+
result: null,
|
|
665
|
+
error: null,
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
eventTimestamp: Date.now(),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
return { executionContext, result: { status: 'success', output: result?.result } };
|
|
672
|
+
},
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
Object.assign(executionContext, res.executionContext);
|
|
676
|
+
return res.result as StepResult<any>;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const stepRes = await this.inngestStep.run(`workflow.${executionContext.workflowId}.step.${step.id}`, async () => {
|
|
680
|
+
let execResults: any;
|
|
681
|
+
let suspended: { payload: any } | undefined;
|
|
682
|
+
try {
|
|
683
|
+
const result = await step.execute({
|
|
684
|
+
mastra: this.mastra!,
|
|
685
|
+
runtimeContext,
|
|
686
|
+
inputData: prevOutput,
|
|
687
|
+
resumeData: resume?.steps[0] === step.id ? resume?.resumePayload : undefined,
|
|
688
|
+
getInitData: () => stepResults?.input as any,
|
|
689
|
+
getStepResult: (step: any) => {
|
|
690
|
+
const result = stepResults[step.id];
|
|
691
|
+
if (result?.status === 'success') {
|
|
692
|
+
return result.output;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return null;
|
|
696
|
+
},
|
|
697
|
+
suspend: async (suspendPayload: any) => {
|
|
698
|
+
executionContext.suspendedPaths[step.id] = executionContext.executionPath;
|
|
699
|
+
suspended = { payload: suspendPayload };
|
|
700
|
+
},
|
|
701
|
+
resume: {
|
|
702
|
+
steps: resume?.steps?.slice(1) || [],
|
|
703
|
+
resumePayload: resume?.resumePayload,
|
|
704
|
+
// @ts-ignore
|
|
705
|
+
runId: stepResults[step.id]?.payload?.__workflow_meta?.runId,
|
|
706
|
+
},
|
|
707
|
+
emitter,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
execResults = { status: 'success', output: result };
|
|
711
|
+
} catch (e) {
|
|
712
|
+
execResults = { status: 'failed', error: e instanceof Error ? e.message : String(e) };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (suspended) {
|
|
716
|
+
execResults = { status: 'suspended', payload: suspended.payload };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (execResults.status === 'failed') {
|
|
720
|
+
if (executionContext.retryConfig.attempts > 0 && this.inngestAttempts < executionContext.retryConfig.attempts) {
|
|
721
|
+
throw execResults.error;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
await emitter.emit('watch', {
|
|
726
|
+
type: 'watch',
|
|
727
|
+
payload: {
|
|
728
|
+
currentStep: {
|
|
729
|
+
id: step.id,
|
|
730
|
+
status: execResults.status,
|
|
731
|
+
output: execResults.output,
|
|
732
|
+
},
|
|
733
|
+
workflowState: {
|
|
734
|
+
status: 'running',
|
|
735
|
+
steps: stepResults,
|
|
736
|
+
result: null,
|
|
737
|
+
error: null,
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
eventTimestamp: Date.now(),
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
return { result: execResults, executionContext, stepResults };
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// @ts-ignore
|
|
747
|
+
Object.assign(executionContext.suspendedPaths, stepRes.executionContext.suspendedPaths);
|
|
748
|
+
// @ts-ignore
|
|
749
|
+
Object.assign(stepResults, stepRes.stepResults);
|
|
750
|
+
|
|
751
|
+
// @ts-ignore
|
|
752
|
+
return stepRes.result;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async persistStepUpdate({
|
|
756
|
+
workflowId,
|
|
757
|
+
runId,
|
|
758
|
+
stepResults,
|
|
759
|
+
executionContext,
|
|
760
|
+
}: {
|
|
761
|
+
workflowId: string;
|
|
762
|
+
runId: string;
|
|
763
|
+
stepResults: Record<string, StepResult<any>>;
|
|
764
|
+
executionContext: ExecutionContext;
|
|
765
|
+
}) {
|
|
766
|
+
await this.inngestStep.run(
|
|
767
|
+
`workflow.${workflowId}.run.${runId}.path.${JSON.stringify(executionContext.executionPath)}.stepUpdate`,
|
|
768
|
+
async () => {
|
|
769
|
+
await this.mastra?.getStorage()?.persistWorkflowSnapshot({
|
|
770
|
+
workflowName: workflowId,
|
|
771
|
+
runId,
|
|
772
|
+
snapshot: {
|
|
773
|
+
runId,
|
|
774
|
+
value: {},
|
|
775
|
+
context: stepResults as any,
|
|
776
|
+
activePaths: [],
|
|
777
|
+
suspendedPaths: executionContext.suspendedPaths,
|
|
778
|
+
// @ts-ignore
|
|
779
|
+
timestamp: Date.now(),
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
},
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async executeConditional({
|
|
787
|
+
workflowId,
|
|
788
|
+
runId,
|
|
789
|
+
entry,
|
|
790
|
+
prevOutput,
|
|
791
|
+
prevStep,
|
|
792
|
+
stepResults,
|
|
793
|
+
resume,
|
|
794
|
+
executionContext,
|
|
795
|
+
emitter,
|
|
796
|
+
runtimeContext,
|
|
797
|
+
}: {
|
|
798
|
+
workflowId: string;
|
|
799
|
+
runId: string;
|
|
800
|
+
entry: { type: 'conditional'; steps: StepFlowEntry[]; conditions: ExecuteFunction<any, any, any, any>[] };
|
|
801
|
+
prevStep: StepFlowEntry;
|
|
802
|
+
prevOutput: any;
|
|
803
|
+
stepResults: Record<string, StepResult<any>>;
|
|
804
|
+
resume?: {
|
|
805
|
+
steps: string[];
|
|
806
|
+
stepResults: Record<string, StepResult<any>>;
|
|
807
|
+
resumePayload: any;
|
|
808
|
+
resumePath: number[];
|
|
809
|
+
};
|
|
810
|
+
executionContext: ExecutionContext;
|
|
811
|
+
emitter: { emit: (event: string, data: any) => Promise<void> };
|
|
812
|
+
runtimeContext: RuntimeContext;
|
|
813
|
+
}): Promise<StepResult<any>> {
|
|
814
|
+
let execResults: any;
|
|
815
|
+
const truthyIndexes = (
|
|
816
|
+
await Promise.all(
|
|
817
|
+
entry.conditions.map((cond, index) =>
|
|
818
|
+
this.inngestStep.run(`workflow.${workflowId}.conditional.${index}`, async () => {
|
|
819
|
+
try {
|
|
820
|
+
const result = await cond({
|
|
821
|
+
mastra: this.mastra!,
|
|
822
|
+
runtimeContext,
|
|
823
|
+
inputData: prevOutput,
|
|
824
|
+
getInitData: () => stepResults?.input as any,
|
|
825
|
+
getStepResult: (step: any) => {
|
|
826
|
+
if (!step?.id) {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const result = stepResults[step.id];
|
|
831
|
+
if (result?.status === 'success') {
|
|
832
|
+
return result.output;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return null;
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
// TODO: this function shouldn't have suspend probably?
|
|
839
|
+
suspend: async (_suspendPayload: any) => {},
|
|
840
|
+
emitter,
|
|
841
|
+
});
|
|
842
|
+
return result ? index : null;
|
|
843
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
844
|
+
} catch (e: unknown) {
|
|
845
|
+
e;
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
}),
|
|
849
|
+
),
|
|
850
|
+
)
|
|
851
|
+
).filter((index: any): index is number => index !== null);
|
|
852
|
+
|
|
853
|
+
const stepsToRun = entry.steps.filter((_, index) => truthyIndexes.includes(index));
|
|
854
|
+
const results: StepResult<any>[] = await Promise.all(
|
|
855
|
+
stepsToRun.map((step, index) =>
|
|
856
|
+
this.executeEntry({
|
|
857
|
+
workflowId,
|
|
858
|
+
runId,
|
|
859
|
+
entry: step,
|
|
860
|
+
prevStep,
|
|
861
|
+
stepResults,
|
|
862
|
+
resume,
|
|
863
|
+
executionContext: {
|
|
864
|
+
workflowId,
|
|
865
|
+
runId,
|
|
866
|
+
executionPath: [...executionContext.executionPath, index],
|
|
867
|
+
suspendedPaths: executionContext.suspendedPaths,
|
|
868
|
+
retryConfig: executionContext.retryConfig,
|
|
869
|
+
executionSpan: executionContext.executionSpan,
|
|
870
|
+
},
|
|
871
|
+
emitter,
|
|
872
|
+
runtimeContext,
|
|
873
|
+
}),
|
|
874
|
+
),
|
|
875
|
+
);
|
|
876
|
+
const hasFailed = results.find(result => result.status === 'failed');
|
|
877
|
+
const hasSuspended = results.find(result => result.status === 'suspended');
|
|
878
|
+
if (hasFailed) {
|
|
879
|
+
execResults = { status: 'failed', error: hasFailed.error };
|
|
880
|
+
} else if (hasSuspended) {
|
|
881
|
+
execResults = { status: 'suspended', payload: hasSuspended.payload };
|
|
882
|
+
} else {
|
|
883
|
+
execResults = {
|
|
884
|
+
status: 'success',
|
|
885
|
+
output: results.reduce((acc: Record<string, any>, result, index) => {
|
|
886
|
+
if (result.status === 'success') {
|
|
887
|
+
// @ts-ignore
|
|
888
|
+
acc[stepsToRun[index]!.step.id] = result.output;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return acc;
|
|
892
|
+
}, {}),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return execResults;
|
|
897
|
+
}
|
|
898
|
+
}
|