@voyantjs/workflows 0.6.7 → 0.6.9
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/dist/auth/index.d.ts +26 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +137 -0
- package/dist/conditions.d.ts +29 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +5 -0
- package/dist/handler/index.d.ts +104 -0
- package/dist/handler/index.d.ts.map +1 -0
- package/dist/handler/index.js +238 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/protocol/index.d.ts +187 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +7 -0
- package/dist/rate-limit/index.d.ts +40 -0
- package/dist/rate-limit/index.d.ts.map +1 -0
- package/dist/rate-limit/index.js +139 -0
- package/dist/runtime/ctx.d.ts +102 -0
- package/dist/runtime/ctx.d.ts.map +1 -0
- package/dist/runtime/ctx.js +607 -0
- package/dist/runtime/determinism.d.ts +19 -0
- package/dist/runtime/determinism.d.ts.map +1 -0
- package/dist/runtime/determinism.js +61 -0
- package/dist/runtime/errors.d.ts +21 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +45 -0
- package/dist/runtime/executor.d.ts +159 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +225 -0
- package/dist/runtime/journal.d.ts +55 -0
- package/dist/runtime/journal.d.ts.map +1 -0
- package/dist/runtime/journal.js +28 -0
- package/dist/testing/index.d.ts +117 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +595 -0
- package/dist/trigger.d.ts +122 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +23 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/workflow.d.ts +212 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +46 -0
- package/package.json +30 -30
- package/src/auth/index.ts +46 -52
- package/src/conditions.ts +13 -13
- package/src/handler/index.ts +110 -106
- package/src/index.ts +7 -7
- package/src/protocol/index.ts +137 -71
- package/src/rate-limit/index.ts +77 -78
- package/src/runtime/ctx.ts +354 -342
- package/src/runtime/determinism.ts +27 -27
- package/src/runtime/errors.ts +17 -17
- package/src/runtime/executor.ts +179 -172
- package/src/runtime/journal.ts +25 -25
- package/src/testing/index.ts +268 -202
- package/src/trigger.ts +64 -71
- package/src/types.ts +16 -18
- package/src/workflow.ts +154 -152
package/src/runtime/executor.ts
CHANGED
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
// Corresponds to the tenant-side handler of POST /__voyant/workflow-step
|
|
5
5
|
// described in docs/runtime-protocol.md §2.1.
|
|
6
6
|
|
|
7
|
-
import type {
|
|
8
|
-
import type
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
7
|
+
import type { SerializedError } from "../protocol/index.js"
|
|
8
|
+
import { durationToMs, type RateLimiter } from "../rate-limit/index.js"
|
|
9
|
+
import type { RunStatus, RunTrigger, WaitpointKind } from "../types.js"
|
|
10
|
+
import type { StepOptions, WorkflowDefinition } from "../workflow.js"
|
|
11
|
+
import { buildCtx, type RuntimeCallbacks, type RuntimeEnvironment } from "./ctx.js"
|
|
12
|
+
import { createClock, createRandom } from "./determinism.js"
|
|
13
13
|
import {
|
|
14
|
-
WaitpointPendingSignal,
|
|
15
|
-
RunCancelledSignal,
|
|
16
|
-
isWaitpointPending,
|
|
17
|
-
isRunCancelled,
|
|
18
14
|
isCompensateRequested,
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
isRunCancelled,
|
|
16
|
+
isWaitpointPending,
|
|
17
|
+
RunCancelledSignal,
|
|
18
|
+
WaitpointPendingSignal,
|
|
19
|
+
} from "./errors.js"
|
|
20
|
+
import type { JournalSlice, StepJournalEntry } from "./journal.js"
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export type StepRunner = (
|
|
23
23
|
/**
|
|
24
24
|
* Executes a step body and returns the journal entry to record.
|
|
25
25
|
*
|
|
@@ -29,22 +29,22 @@ export interface StepRunner {
|
|
|
29
29
|
* and use the run/workflow identity + step options to address the
|
|
30
30
|
* remote container and POST the required context.
|
|
31
31
|
*/
|
|
32
|
-
|
|
33
|
-
stepId: string
|
|
34
|
-
attempt: number
|
|
35
|
-
input: unknown
|
|
36
|
-
fn: (stepCtx: import("../workflow.js").StepContext) => Promise<unknown
|
|
37
|
-
stepCtx: import("../workflow.js").StepContext
|
|
32
|
+
args: {
|
|
33
|
+
stepId: string
|
|
34
|
+
attempt: number
|
|
35
|
+
input: unknown
|
|
36
|
+
fn: (stepCtx: import("../workflow.js").StepContext) => Promise<unknown>
|
|
37
|
+
stepCtx: import("../workflow.js").StepContext
|
|
38
38
|
/** Identity of the run — used by dispatching runners. */
|
|
39
|
-
runId: string
|
|
40
|
-
workflowId: string
|
|
41
|
-
workflowVersion: string
|
|
39
|
+
runId: string
|
|
40
|
+
workflowId: string
|
|
41
|
+
workflowVersion: string
|
|
42
42
|
/** Project / organization id from the runtime environment — used by
|
|
43
43
|
* dispatching runners to resolve per-tenant bundle storage keys. */
|
|
44
|
-
projectId: string
|
|
45
|
-
organizationId: string
|
|
44
|
+
projectId: string
|
|
45
|
+
organizationId: string
|
|
46
46
|
/** Merged step options (runtime, machine, timeout, …). */
|
|
47
|
-
options: import("../workflow.js").StepOptions<unknown
|
|
47
|
+
options: import("../workflow.js").StepOptions<unknown>
|
|
48
48
|
/**
|
|
49
49
|
* Current journal slice at dispatch time — steps already completed,
|
|
50
50
|
* waitpoints already resolved, etc. Dispatching runners pass this
|
|
@@ -52,58 +52,58 @@ export interface StepRunner {
|
|
|
52
52
|
* cached steps, and the container can stop cleanly after the
|
|
53
53
|
* target step runs.
|
|
54
54
|
*/
|
|
55
|
-
journal: JournalSlice
|
|
56
|
-
}
|
|
57
|
-
|
|
55
|
+
journal: JournalSlice
|
|
56
|
+
},
|
|
57
|
+
) => Promise<StepJournalEntry>
|
|
58
58
|
|
|
59
59
|
export interface WaitpointRegistration {
|
|
60
|
-
clientWaitpointId: string
|
|
61
|
-
kind: WaitpointKind
|
|
62
|
-
meta: Record<string, unknown
|
|
63
|
-
timeoutMs?: number
|
|
60
|
+
clientWaitpointId: string
|
|
61
|
+
kind: WaitpointKind
|
|
62
|
+
meta: Record<string, unknown>
|
|
63
|
+
timeoutMs?: number
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export interface MetadataMutation {
|
|
67
|
-
op: "set" | "increment" | "append" | "remove"
|
|
68
|
-
key: string
|
|
69
|
-
value?: unknown
|
|
70
|
-
target?: "self" | "parent" | "root"
|
|
67
|
+
op: "set" | "increment" | "append" | "remove"
|
|
68
|
+
key: string
|
|
69
|
+
value?: unknown
|
|
70
|
+
target?: "self" | "parent" | "root"
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export interface CompensationReport {
|
|
74
|
-
stepId: string
|
|
75
|
-
status: "ok" | "err"
|
|
76
|
-
error?: SerializedError
|
|
77
|
-
durationMs: number
|
|
74
|
+
stepId: string
|
|
75
|
+
status: "ok" | "err"
|
|
76
|
+
error?: SerializedError
|
|
77
|
+
durationMs: number
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export interface StreamChunk {
|
|
81
|
-
streamId: string
|
|
82
|
-
seq: number
|
|
83
|
-
encoding: "text" | "json" | "base64"
|
|
84
|
-
chunk: unknown
|
|
85
|
-
final: boolean
|
|
86
|
-
at: number
|
|
81
|
+
streamId: string
|
|
82
|
+
seq: number
|
|
83
|
+
encoding: "text" | "json" | "base64"
|
|
84
|
+
chunk: unknown
|
|
85
|
+
final: boolean
|
|
86
|
+
at: number
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export interface ExecuteWorkflowStepRequest {
|
|
90
|
-
runId: string
|
|
91
|
-
workflowId: string
|
|
92
|
-
workflowVersion: string
|
|
93
|
-
input: unknown
|
|
94
|
-
journal: JournalSlice
|
|
95
|
-
invocationCount: number
|
|
96
|
-
environment: RuntimeEnvironment
|
|
97
|
-
triggeredBy: RunTrigger
|
|
98
|
-
runStartedAt: number
|
|
99
|
-
tags: string[]
|
|
100
|
-
abortSignal?: AbortSignal
|
|
90
|
+
runId: string
|
|
91
|
+
workflowId: string
|
|
92
|
+
workflowVersion: string
|
|
93
|
+
input: unknown
|
|
94
|
+
journal: JournalSlice
|
|
95
|
+
invocationCount: number
|
|
96
|
+
environment: RuntimeEnvironment
|
|
97
|
+
triggeredBy: RunTrigger
|
|
98
|
+
runStartedAt: number
|
|
99
|
+
tags: string[]
|
|
100
|
+
abortSignal?: AbortSignal
|
|
101
101
|
/**
|
|
102
102
|
* Default step executor (the "edge" runtime) — runs step bodies
|
|
103
103
|
* in-process. Used for any step whose `options.runtime` is unset or
|
|
104
104
|
* explicitly `"edge"`.
|
|
105
105
|
*/
|
|
106
|
-
stepRunner: StepRunner
|
|
106
|
+
stepRunner: StepRunner
|
|
107
107
|
/**
|
|
108
108
|
* Optional runner for steps declared with `options.runtime === "node"`.
|
|
109
109
|
* Typical impl dispatches to a Cloudflare Container sized for the
|
|
@@ -113,7 +113,7 @@ export interface ExecuteWorkflowStepRequest {
|
|
|
113
113
|
* `NODE_RUNTIME_UNAVAILABLE` — declaring a runtime and then silently
|
|
114
114
|
* falling back to edge would hide deployment bugs.
|
|
115
115
|
*/
|
|
116
|
-
nodeStepRunner?: StepRunner
|
|
116
|
+
nodeStepRunner?: StepRunner
|
|
117
117
|
/**
|
|
118
118
|
* Optional rate limiter. When a step declares `options.rateLimit`,
|
|
119
119
|
* the executor calls `rateLimiter.acquire(...)` before invoking the
|
|
@@ -121,9 +121,9 @@ export interface ExecuteWorkflowStepRequest {
|
|
|
121
121
|
* fails with `RATE_LIMITER_MISSING` — declaring a limit and not
|
|
122
122
|
* enforcing it would be silently dangerous.
|
|
123
123
|
*/
|
|
124
|
-
rateLimiter?: RateLimiter
|
|
124
|
+
rateLimiter?: RateLimiter
|
|
125
125
|
/** `() => number` used for compensation durations. Defaults to Date.now. */
|
|
126
|
-
now?: () => number
|
|
126
|
+
now?: () => number
|
|
127
127
|
/**
|
|
128
128
|
* Optional per-chunk callback fired synchronously from
|
|
129
129
|
* `ctx.stream.*` as each chunk is produced. Enables live streaming
|
|
@@ -131,75 +131,77 @@ export interface ExecuteWorkflowStepRequest {
|
|
|
131
131
|
* Chunks are still accumulated in the response's `streamChunks`
|
|
132
132
|
* array so the at-end delivery keeps working.
|
|
133
133
|
*/
|
|
134
|
-
onStreamChunk?: (chunk: StreamChunk) => void
|
|
134
|
+
onStreamChunk?: (chunk: StreamChunk) => void
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export type ExecuteWorkflowStepResponse =
|
|
138
138
|
| {
|
|
139
|
-
status: "completed"
|
|
140
|
-
output: unknown
|
|
141
|
-
metadataUpdates: MetadataMutation[]
|
|
142
|
-
journal: JournalSlice
|
|
143
|
-
streamChunks: StreamChunk[]
|
|
139
|
+
status: "completed"
|
|
140
|
+
output: unknown
|
|
141
|
+
metadataUpdates: MetadataMutation[]
|
|
142
|
+
journal: JournalSlice
|
|
143
|
+
streamChunks: StreamChunk[]
|
|
144
144
|
}
|
|
145
145
|
| {
|
|
146
|
-
status: "failed"
|
|
147
|
-
error: SerializedError
|
|
148
|
-
metadataUpdates: MetadataMutation[]
|
|
149
|
-
journal: JournalSlice
|
|
150
|
-
streamChunks: StreamChunk[]
|
|
146
|
+
status: "failed"
|
|
147
|
+
error: SerializedError
|
|
148
|
+
metadataUpdates: MetadataMutation[]
|
|
149
|
+
journal: JournalSlice
|
|
150
|
+
streamChunks: StreamChunk[]
|
|
151
151
|
}
|
|
152
152
|
| {
|
|
153
|
-
status: "cancelled"
|
|
154
|
-
metadataUpdates: MetadataMutation[]
|
|
155
|
-
journal: JournalSlice
|
|
156
|
-
compensations: CompensationReport[]
|
|
157
|
-
streamChunks: StreamChunk[]
|
|
153
|
+
status: "cancelled"
|
|
154
|
+
metadataUpdates: MetadataMutation[]
|
|
155
|
+
journal: JournalSlice
|
|
156
|
+
compensations: CompensationReport[]
|
|
157
|
+
streamChunks: StreamChunk[]
|
|
158
158
|
}
|
|
159
159
|
| {
|
|
160
|
-
status: "waiting"
|
|
161
|
-
waitpoints: WaitpointRegistration[]
|
|
162
|
-
metadataUpdates: MetadataMutation[]
|
|
163
|
-
journal: JournalSlice
|
|
164
|
-
streamChunks: StreamChunk[]
|
|
160
|
+
status: "waiting"
|
|
161
|
+
waitpoints: WaitpointRegistration[]
|
|
162
|
+
metadataUpdates: MetadataMutation[]
|
|
163
|
+
journal: JournalSlice
|
|
164
|
+
streamChunks: StreamChunk[]
|
|
165
165
|
}
|
|
166
166
|
| {
|
|
167
|
-
status: "compensated"
|
|
167
|
+
status: "compensated"
|
|
168
168
|
/** Only set when compensation was triggered by an uncaught body error. */
|
|
169
|
-
error?: SerializedError
|
|
170
|
-
compensations: CompensationReport[]
|
|
171
|
-
metadataUpdates: MetadataMutation[]
|
|
172
|
-
journal: JournalSlice
|
|
173
|
-
streamChunks: StreamChunk[]
|
|
169
|
+
error?: SerializedError
|
|
170
|
+
compensations: CompensationReport[]
|
|
171
|
+
metadataUpdates: MetadataMutation[]
|
|
172
|
+
journal: JournalSlice
|
|
173
|
+
streamChunks: StreamChunk[]
|
|
174
174
|
}
|
|
175
175
|
| {
|
|
176
|
-
status: "compensation_failed"
|
|
177
|
-
error?: SerializedError
|
|
178
|
-
compensations: CompensationReport[]
|
|
179
|
-
metadataUpdates: MetadataMutation[]
|
|
180
|
-
journal: JournalSlice
|
|
181
|
-
streamChunks: StreamChunk[]
|
|
182
|
-
}
|
|
176
|
+
status: "compensation_failed"
|
|
177
|
+
error?: SerializedError
|
|
178
|
+
compensations: CompensationReport[]
|
|
179
|
+
metadataUpdates: MetadataMutation[]
|
|
180
|
+
journal: JournalSlice
|
|
181
|
+
streamChunks: StreamChunk[]
|
|
182
|
+
}
|
|
183
183
|
|
|
184
184
|
interface Compensable {
|
|
185
|
-
stepId: string
|
|
186
|
-
output: unknown
|
|
187
|
-
compensate: (output: unknown) => Promise<void
|
|
185
|
+
stepId: string
|
|
186
|
+
output: unknown
|
|
187
|
+
compensate: (output: unknown) => Promise<void>
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
export async function executeWorkflowStep(
|
|
191
191
|
def: WorkflowDefinition,
|
|
192
192
|
req: ExecuteWorkflowStepRequest,
|
|
193
193
|
): Promise<ExecuteWorkflowStepResponse> {
|
|
194
|
-
const abortSignal = req.abortSignal ?? new AbortController().signal
|
|
195
|
-
const now = req.now ?? (() => Date.now())
|
|
196
|
-
const clock = createClock(req.runStartedAt)
|
|
197
|
-
const random = createRandom(req.runId)
|
|
198
|
-
const waitpoints: WaitpointRegistration[] = []
|
|
199
|
-
const metadataUpdates: MetadataMutation[] = []
|
|
200
|
-
const compensable: Compensable[] = []
|
|
201
|
-
const streamChunks: StreamChunk[] = []
|
|
202
|
-
const retryOverride: { current: import("../types.js").RetryPolicy | undefined } = {
|
|
194
|
+
const abortSignal = req.abortSignal ?? new AbortController().signal
|
|
195
|
+
const now = req.now ?? (() => Date.now())
|
|
196
|
+
const clock = createClock(req.runStartedAt)
|
|
197
|
+
const random = createRandom(req.runId)
|
|
198
|
+
const waitpoints: WaitpointRegistration[] = []
|
|
199
|
+
const metadataUpdates: MetadataMutation[] = []
|
|
200
|
+
const compensable: Compensable[] = []
|
|
201
|
+
const streamChunks: StreamChunk[] = []
|
|
202
|
+
const retryOverride: { current: import("../types.js").RetryPolicy | undefined } = {
|
|
203
|
+
current: def.config.retry,
|
|
204
|
+
}
|
|
203
205
|
|
|
204
206
|
const callbacks: RuntimeCallbacks = {
|
|
205
207
|
invocationCount: req.invocationCount,
|
|
@@ -214,17 +216,17 @@ export async function executeWorkflowStep(
|
|
|
214
216
|
projectId: req.environment.project.id,
|
|
215
217
|
limiter: req.rateLimiter,
|
|
216
218
|
signal: abortSignal,
|
|
217
|
-
})
|
|
219
|
+
})
|
|
218
220
|
}
|
|
219
|
-
const runtime = args.options.runtime ?? "edge"
|
|
220
|
-
const runner = runtime === "node" ? req.nodeStepRunner : req.stepRunner
|
|
221
|
+
const runtime = args.options.runtime ?? "edge"
|
|
222
|
+
const runner = runtime === "node" ? req.nodeStepRunner : req.stepRunner
|
|
221
223
|
if (!runner) {
|
|
222
224
|
const e = new Error(
|
|
223
225
|
`step "${args.stepId}" declared runtime="node" but the handler has no nodeStepRunner wired; ` +
|
|
224
226
|
`pass { nodeStepRunner } to createStepHandler() or remove options.runtime`,
|
|
225
|
-
)
|
|
226
|
-
(e as Error & { code?: string }).code = "NODE_RUNTIME_UNAVAILABLE"
|
|
227
|
-
throw e
|
|
227
|
+
)
|
|
228
|
+
;(e as Error & { code?: string }).code = "NODE_RUNTIME_UNAVAILABLE"
|
|
229
|
+
throw e
|
|
228
230
|
}
|
|
229
231
|
const entry = await runner({
|
|
230
232
|
stepId: args.stepId,
|
|
@@ -239,42 +241,42 @@ export async function executeWorkflowStep(
|
|
|
239
241
|
organizationId: req.environment.organization.id,
|
|
240
242
|
options: args.options,
|
|
241
243
|
journal: req.journal,
|
|
242
|
-
})
|
|
244
|
+
})
|
|
243
245
|
// Stamp the runtime on the journal entry so downstream consumers
|
|
244
246
|
// (journal persistence, dashboard events) can report where each
|
|
245
247
|
// step actually ran.
|
|
246
|
-
entry.runtime = runtime
|
|
247
|
-
return entry
|
|
248
|
+
entry.runtime = runtime
|
|
249
|
+
return entry
|
|
248
250
|
},
|
|
249
251
|
registerWaitpoint(args) {
|
|
250
|
-
waitpoints.push(args)
|
|
252
|
+
waitpoints.push(args)
|
|
251
253
|
},
|
|
252
254
|
pushMetadata(op) {
|
|
253
|
-
metadataUpdates.push(op)
|
|
255
|
+
metadataUpdates.push(op)
|
|
254
256
|
},
|
|
255
257
|
recordCompensable(args) {
|
|
256
|
-
compensable.push(args)
|
|
258
|
+
compensable.push(args)
|
|
257
259
|
},
|
|
258
260
|
compensableLength(): number {
|
|
259
|
-
return compensable.length
|
|
261
|
+
return compensable.length
|
|
260
262
|
},
|
|
261
263
|
spliceCompensable(fromIndex: number): Compensable[] {
|
|
262
|
-
return compensable.splice(fromIndex)
|
|
264
|
+
return compensable.splice(fromIndex)
|
|
263
265
|
},
|
|
264
266
|
pushStreamChunk(args) {
|
|
265
|
-
const chunk = { ...args, at: now() }
|
|
266
|
-
streamChunks.push(chunk)
|
|
267
|
+
const chunk = { ...args, at: now() }
|
|
268
|
+
streamChunks.push(chunk)
|
|
267
269
|
// Fire the live hook synchronously. Errors are swallowed — a
|
|
268
270
|
// misbehaving subscriber must not break the workflow body.
|
|
269
271
|
if (req.onStreamChunk) {
|
|
270
272
|
try {
|
|
271
|
-
req.onStreamChunk(chunk)
|
|
273
|
+
req.onStreamChunk(chunk)
|
|
272
274
|
} catch {
|
|
273
275
|
/* ignore */
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
},
|
|
277
|
-
}
|
|
279
|
+
}
|
|
278
280
|
|
|
279
281
|
const ctx = buildCtx({
|
|
280
282
|
env: req.environment,
|
|
@@ -283,48 +285,54 @@ export async function executeWorkflowStep(
|
|
|
283
285
|
clock,
|
|
284
286
|
random,
|
|
285
287
|
retryOverride,
|
|
286
|
-
})
|
|
288
|
+
})
|
|
287
289
|
|
|
288
290
|
try {
|
|
289
|
-
const output = await def.config.run(req.input, ctx)
|
|
291
|
+
const output = await def.config.run(req.input, ctx)
|
|
290
292
|
// If the body registered a waitpoint but a user try/catch swallowed
|
|
291
293
|
// the internal yield signal, honour the waitpoint — the body can't
|
|
292
294
|
// both register a waitpoint and complete in the same invocation.
|
|
293
295
|
if (waitpoints.length > 0) {
|
|
294
|
-
return { status: "waiting", waitpoints, metadataUpdates, journal: req.journal, streamChunks }
|
|
296
|
+
return { status: "waiting", waitpoints, metadataUpdates, journal: req.journal, streamChunks }
|
|
295
297
|
}
|
|
296
|
-
return { status: "completed", output, metadataUpdates, journal: req.journal, streamChunks }
|
|
298
|
+
return { status: "completed", output, metadataUpdates, journal: req.journal, streamChunks }
|
|
297
299
|
} catch (err) {
|
|
298
300
|
if (isWaitpointPending(err)) {
|
|
299
|
-
return { status: "waiting", waitpoints, metadataUpdates, journal: req.journal, streamChunks }
|
|
301
|
+
return { status: "waiting", waitpoints, metadataUpdates, journal: req.journal, streamChunks }
|
|
300
302
|
}
|
|
301
303
|
// Same guard for the error path: a swallowed signal shouldn't let
|
|
302
304
|
// the body claim failure while waitpoints are pending.
|
|
303
305
|
if (waitpoints.length > 0 && !isRunCancelled(err) && !isCompensateRequested(err)) {
|
|
304
|
-
return { status: "waiting", waitpoints, metadataUpdates, journal: req.journal, streamChunks }
|
|
306
|
+
return { status: "waiting", waitpoints, metadataUpdates, journal: req.journal, streamChunks }
|
|
305
307
|
}
|
|
306
308
|
if (isRunCancelled(err)) {
|
|
307
309
|
// Default: compensate on cancel. Terminal status stays `cancelled`
|
|
308
310
|
// to reflect why the run ended.
|
|
309
|
-
const compensations = await runCompensations(compensable, now)
|
|
310
|
-
return {
|
|
311
|
+
const compensations = await runCompensations(compensable, now)
|
|
312
|
+
return {
|
|
313
|
+
status: "cancelled",
|
|
314
|
+
metadataUpdates,
|
|
315
|
+
journal: req.journal,
|
|
316
|
+
compensations,
|
|
317
|
+
streamChunks,
|
|
318
|
+
}
|
|
311
319
|
}
|
|
312
320
|
if (isCompensateRequested(err)) {
|
|
313
|
-
const compensations = await runCompensations(compensable, now)
|
|
314
|
-
const anyErr = compensations.some((c) => c.status === "err")
|
|
321
|
+
const compensations = await runCompensations(compensable, now)
|
|
322
|
+
const anyErr = compensations.some((c) => c.status === "err")
|
|
315
323
|
return {
|
|
316
324
|
status: anyErr ? "compensation_failed" : "compensated",
|
|
317
325
|
compensations,
|
|
318
326
|
metadataUpdates,
|
|
319
327
|
journal: req.journal,
|
|
320
328
|
streamChunks,
|
|
321
|
-
}
|
|
329
|
+
}
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
// Uncaught user error. Run compensations LIFO; adjust terminal status
|
|
325
333
|
// based on whether compensations were registered and all succeeded.
|
|
326
|
-
const compensations = await runCompensations(compensable, now)
|
|
327
|
-
const serialized = serializeError(err)
|
|
334
|
+
const compensations = await runCompensations(compensable, now)
|
|
335
|
+
const serialized = serializeError(err)
|
|
328
336
|
|
|
329
337
|
if (compensations.length === 0) {
|
|
330
338
|
return {
|
|
@@ -333,10 +341,10 @@ export async function executeWorkflowStep(
|
|
|
333
341
|
metadataUpdates,
|
|
334
342
|
journal: req.journal,
|
|
335
343
|
streamChunks,
|
|
336
|
-
}
|
|
344
|
+
}
|
|
337
345
|
}
|
|
338
346
|
|
|
339
|
-
const anyErr = compensations.some((c) => c.status === "err")
|
|
347
|
+
const anyErr = compensations.some((c) => c.status === "err")
|
|
340
348
|
return {
|
|
341
349
|
status: anyErr ? "compensation_failed" : "compensated",
|
|
342
350
|
error: serialized,
|
|
@@ -344,7 +352,7 @@ export async function executeWorkflowStep(
|
|
|
344
352
|
metadataUpdates,
|
|
345
353
|
journal: req.journal,
|
|
346
354
|
streamChunks,
|
|
347
|
-
}
|
|
355
|
+
}
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
358
|
|
|
@@ -352,31 +360,31 @@ async function runCompensations(
|
|
|
352
360
|
compensable: readonly Compensable[],
|
|
353
361
|
now: () => number,
|
|
354
362
|
): Promise<CompensationReport[]> {
|
|
355
|
-
const reports: CompensationReport[] = []
|
|
363
|
+
const reports: CompensationReport[] = []
|
|
356
364
|
// Reverse order: last-completed compensates first.
|
|
357
365
|
for (let i = compensable.length - 1; i >= 0; i--) {
|
|
358
|
-
const c = compensable[i]
|
|
359
|
-
const startedAt = now()
|
|
366
|
+
const c = compensable[i]!
|
|
367
|
+
const startedAt = now()
|
|
360
368
|
try {
|
|
361
|
-
await c.compensate(c.output)
|
|
362
|
-
reports.push({ stepId: c.stepId, status: "ok", durationMs: now() - startedAt })
|
|
369
|
+
await c.compensate(c.output)
|
|
370
|
+
reports.push({ stepId: c.stepId, status: "ok", durationMs: now() - startedAt })
|
|
363
371
|
} catch (err) {
|
|
364
372
|
reports.push({
|
|
365
373
|
stepId: c.stepId,
|
|
366
374
|
status: "err",
|
|
367
375
|
error: serializeError(err),
|
|
368
376
|
durationMs: now() - startedAt,
|
|
369
|
-
})
|
|
377
|
+
})
|
|
370
378
|
// Continue with remaining compensations; don't abort on one failure.
|
|
371
379
|
}
|
|
372
380
|
}
|
|
373
|
-
return reports
|
|
381
|
+
return reports
|
|
374
382
|
}
|
|
375
383
|
|
|
376
384
|
function serializeError(err: unknown): SerializedError {
|
|
377
385
|
if (err instanceof Error) {
|
|
378
|
-
const code = (err as { code?: string }).code
|
|
379
|
-
const cause = (err as { cause?: unknown }).cause
|
|
386
|
+
const code = (err as { code?: string }).code
|
|
387
|
+
const cause = (err as { cause?: unknown }).cause
|
|
380
388
|
return {
|
|
381
389
|
category: "USER_ERROR",
|
|
382
390
|
code: typeof code === "string" ? code : "UNKNOWN",
|
|
@@ -384,44 +392,43 @@ function serializeError(err: unknown): SerializedError {
|
|
|
384
392
|
name: err.name,
|
|
385
393
|
stack: err.stack,
|
|
386
394
|
cause: cause !== undefined ? serializeError(cause) : undefined,
|
|
387
|
-
}
|
|
395
|
+
}
|
|
388
396
|
}
|
|
389
|
-
return { category: "USER_ERROR", code: "UNKNOWN", message: String(err) }
|
|
397
|
+
return { category: "USER_ERROR", code: "UNKNOWN", message: String(err) }
|
|
390
398
|
}
|
|
391
399
|
|
|
392
400
|
async function acquireRateLimit(args: {
|
|
393
|
-
spec: NonNullable<StepOptions<unknown>["rateLimit"]
|
|
394
|
-
stepId: string
|
|
395
|
-
input: unknown
|
|
396
|
-
runId: string
|
|
397
|
-
projectId: string
|
|
398
|
-
limiter: RateLimiter | undefined
|
|
399
|
-
signal: AbortSignal
|
|
401
|
+
spec: NonNullable<StepOptions<unknown>["rateLimit"]>
|
|
402
|
+
stepId: string
|
|
403
|
+
input: unknown
|
|
404
|
+
runId: string
|
|
405
|
+
projectId: string
|
|
406
|
+
limiter: RateLimiter | undefined
|
|
407
|
+
signal: AbortSignal
|
|
400
408
|
}): Promise<void> {
|
|
401
|
-
const ctx = { run: { id: args.runId }, project: { id: args.projectId } }
|
|
402
|
-
const key =
|
|
403
|
-
typeof args.spec.key === "function" ? args.spec.key(args.input, ctx) : args.spec.key;
|
|
409
|
+
const ctx = { run: { id: args.runId }, project: { id: args.projectId } }
|
|
410
|
+
const key = typeof args.spec.key === "function" ? args.spec.key(args.input, ctx) : args.spec.key
|
|
404
411
|
const limit =
|
|
405
|
-
typeof args.spec.limit === "function" ? args.spec.limit(args.input) : args.spec.limit
|
|
412
|
+
typeof args.spec.limit === "function" ? args.spec.limit(args.input) : args.spec.limit
|
|
406
413
|
const units =
|
|
407
414
|
args.spec.units === undefined
|
|
408
415
|
? 1
|
|
409
416
|
: typeof args.spec.units === "function"
|
|
410
417
|
? args.spec.units(args.input)
|
|
411
|
-
: args.spec.units
|
|
412
|
-
const windowMs = durationToMs(args.spec.window)
|
|
413
|
-
const onLimit = args.spec.onLimit ?? "queue"
|
|
418
|
+
: args.spec.units
|
|
419
|
+
const windowMs = durationToMs(args.spec.window)
|
|
420
|
+
const onLimit = args.spec.onLimit ?? "queue"
|
|
414
421
|
|
|
415
422
|
if (!args.limiter) {
|
|
416
423
|
const e = new Error(
|
|
417
424
|
`step "${args.stepId}" declared options.rateLimit but the handler has no rateLimiter wired; ` +
|
|
418
425
|
`pass { rateLimiter } to createStepHandler()`,
|
|
419
|
-
)
|
|
420
|
-
(e as Error & { code?: string }).code = "RATE_LIMITER_MISSING"
|
|
421
|
-
throw e
|
|
426
|
+
)
|
|
427
|
+
;(e as Error & { code?: string }).code = "RATE_LIMITER_MISSING"
|
|
428
|
+
throw e
|
|
422
429
|
}
|
|
423
|
-
await args.limiter.acquire({ key, limit, units, windowMs, onLimit, signal: args.signal })
|
|
430
|
+
await args.limiter.acquire({ key, limit, units, windowMs, onLimit, signal: args.signal })
|
|
424
431
|
}
|
|
425
432
|
|
|
426
|
-
export {
|
|
427
|
-
export
|
|
433
|
+
export type { RunStatus }
|
|
434
|
+
export { RunCancelledSignal, WaitpointPendingSignal }
|
package/src/runtime/journal.ts
CHANGED
|
@@ -3,45 +3,45 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Wire shape defined in docs/runtime-protocol.md §2.1 (JournalSlice).
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
6
|
+
import type { SerializedError } from "../protocol/index.js"
|
|
7
|
+
import type { WaitpointKind } from "../types.js"
|
|
8
8
|
|
|
9
9
|
export interface StepJournalEntry {
|
|
10
|
-
attempt: number
|
|
11
|
-
status: "ok" | "err"
|
|
12
|
-
output?: unknown
|
|
13
|
-
error?: SerializedError
|
|
14
|
-
startedAt: number
|
|
15
|
-
finishedAt: number
|
|
10
|
+
attempt: number
|
|
11
|
+
status: "ok" | "err"
|
|
12
|
+
output?: unknown
|
|
13
|
+
error?: SerializedError
|
|
14
|
+
startedAt: number
|
|
15
|
+
finishedAt: number
|
|
16
16
|
/**
|
|
17
17
|
* Which runtime actually executed the step. Set by the executor when
|
|
18
18
|
* routing between `stepRunner` (edge) and `nodeStepRunner`.
|
|
19
19
|
* Informational only — doesn't affect replay.
|
|
20
20
|
*/
|
|
21
|
-
runtime?: "edge" | "node"
|
|
21
|
+
runtime?: "edge" | "node"
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface WaitpointResolutionEntry {
|
|
25
|
-
kind: WaitpointKind
|
|
26
|
-
resolvedAt: number
|
|
27
|
-
matchedEventId?: string
|
|
28
|
-
payload?: unknown
|
|
29
|
-
source: "live" | "inbox" | "replay"
|
|
25
|
+
kind: WaitpointKind
|
|
26
|
+
resolvedAt: number
|
|
27
|
+
matchedEventId?: string
|
|
28
|
+
payload?: unknown
|
|
29
|
+
source: "live" | "inbox" | "replay"
|
|
30
30
|
/** Populated for RUN waitpoints when the child run ended in a failure state. */
|
|
31
|
-
error?: SerializedError
|
|
31
|
+
error?: SerializedError
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface CompensationJournalEntry {
|
|
35
|
-
status: "ok" | "err"
|
|
36
|
-
finishedAt: number
|
|
37
|
-
error?: SerializedError
|
|
35
|
+
status: "ok" | "err"
|
|
36
|
+
finishedAt: number
|
|
37
|
+
error?: SerializedError
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface JournalSlice {
|
|
41
|
-
stepResults: Record<string, StepJournalEntry
|
|
42
|
-
waitpointsResolved: Record<string, WaitpointResolutionEntry
|
|
43
|
-
compensationsRun: Record<string, CompensationJournalEntry
|
|
44
|
-
metadataState: Record<string, unknown
|
|
41
|
+
stepResults: Record<string, StepJournalEntry>
|
|
42
|
+
waitpointsResolved: Record<string, WaitpointResolutionEntry>
|
|
43
|
+
compensationsRun: Record<string, CompensationJournalEntry>
|
|
44
|
+
metadataState: Record<string, unknown>
|
|
45
45
|
/**
|
|
46
46
|
* Stream ids whose generator has already been consumed in a prior
|
|
47
47
|
* invocation. The orchestrator already has the chunks on the run
|
|
@@ -49,7 +49,7 @@ export interface JournalSlice {
|
|
|
49
49
|
* (generators often have side effects — LLM calls, file reads,
|
|
50
50
|
* billable API usage).
|
|
51
51
|
*/
|
|
52
|
-
streamsCompleted: Record<string, { chunkCount: number }
|
|
52
|
+
streamsCompleted: Record<string, { chunkCount: number }>
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function emptyJournal(): JournalSlice {
|
|
@@ -59,7 +59,7 @@ export function emptyJournal(): JournalSlice {
|
|
|
59
59
|
compensationsRun: {},
|
|
60
60
|
metadataState: {},
|
|
61
61
|
streamsCompleted: {},
|
|
62
|
-
}
|
|
62
|
+
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
@@ -75,5 +75,5 @@ export function forkJournal(j: JournalSlice): JournalSlice {
|
|
|
75
75
|
compensationsRun: { ...j.compensationsRun },
|
|
76
76
|
metadataState: { ...j.metadataState },
|
|
77
77
|
streamsCompleted: { ...j.streamsCompleted },
|
|
78
|
-
}
|
|
78
|
+
}
|
|
79
79
|
}
|