awaitly 2.0.0 → 3.0.0
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/di-Biw_plBn.d.cts +15 -0
- package/dist/di-DxyH2N3i.d.ts +15 -0
- package/dist/durable.cjs +4670 -0
- package/dist/durable.cjs.map +1 -0
- package/dist/durable.d.cts +9 -0
- package/dist/durable.d.ts +9 -0
- package/dist/durable.js +4610 -0
- package/dist/durable.js.map +1 -0
- package/dist/{di-BbFFfO8y.d.ts → duration-BjVn3QpB.d.cts} +1 -15
- package/dist/{di-BDlT7InM.d.cts → duration-BjVn3QpB.d.ts} +1 -15
- package/dist/engine.cjs +4501 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.cts +113 -0
- package/dist/engine.d.ts +113 -0
- package/dist/engine.js +4474 -0
- package/dist/engine.js.map +1 -0
- package/dist/{errors-DtXvrCiO.d.cts → errors-sDSeaZBO.d.cts} +1 -1
- package/dist/{errors-DtXvrCiO.d.ts → errors-sDSeaZBO.d.ts} +1 -1
- package/dist/guards-CwQZro1F.d.ts +72 -0
- package/dist/guards-DhhOJZda.d.cts +72 -0
- package/dist/hitl-BnnnLzh2.d.cts +468 -0
- package/dist/hitl-pYfkQMv_.d.ts +468 -0
- package/dist/hitl.cjs +646 -0
- package/dist/hitl.cjs.map +1 -0
- package/dist/hitl.d.cts +440 -0
- package/dist/hitl.d.ts +440 -0
- package/dist/hitl.js +605 -0
- package/dist/hitl.js.map +1 -0
- package/dist/index-Bew12SZ8.d.ts +417 -0
- package/dist/index-Bwt-iz50.d.cts +417 -0
- package/dist/{types-B8NfNRGX.d.ts → index-Cv4K_3sZ.d.ts} +2 -1162
- package/dist/{types-BZ2f4MRR.d.cts → index-ICmfQi08.d.cts} +2 -1162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1527
- package/dist/index.d.ts +11 -1527
- package/dist/persistence.cjs +552 -0
- package/dist/persistence.cjs.map +1 -0
- package/dist/persistence.d.cts +254 -0
- package/dist/persistence.d.ts +254 -0
- package/dist/persistence.js +499 -0
- package/dist/persistence.js.map +1 -0
- package/dist/reliability.cjs +1651 -0
- package/dist/reliability.cjs.map +1 -0
- package/dist/reliability.d.cts +1442 -0
- package/dist/reliability.d.ts +1442 -0
- package/dist/reliability.js +1589 -0
- package/dist/reliability.js.map +1 -0
- package/dist/result.d.cts +1 -1
- package/dist/result.d.ts +1 -1
- package/dist/run.cjs +2283 -0
- package/dist/run.cjs.map +1 -0
- package/dist/run.d.cts +90 -0
- package/dist/run.d.ts +90 -0
- package/dist/run.js +2247 -0
- package/dist/run.js.map +1 -0
- package/dist/saga.cjs +3699 -0
- package/dist/saga.cjs.map +1 -0
- package/dist/saga.d.cts +162 -0
- package/dist/saga.d.ts +162 -0
- package/dist/saga.js +3670 -0
- package/dist/saga.js.map +1 -0
- package/dist/store-contract-BI98VYmX.d.ts +66 -0
- package/dist/store-contract-C2HyR_o6.d.cts +66 -0
- package/dist/streaming.cjs +895 -0
- package/dist/streaming.cjs.map +1 -0
- package/dist/streaming.d.cts +594 -0
- package/dist/streaming.d.ts +594 -0
- package/dist/streaming.js +832 -0
- package/dist/streaming.js.map +1 -0
- package/dist/testing.d.cts +4 -2
- package/dist/testing.d.ts +4 -2
- package/dist/types-BDTxgKKc.d.ts +845 -0
- package/dist/types-C9Y71dua.d.cts +845 -0
- package/dist/types-d8q8iQlk.d.cts +323 -0
- package/dist/types-sObbY4mX.d.ts +323 -0
- package/dist/webhook.cjs +461 -0
- package/dist/webhook.cjs.map +1 -0
- package/dist/webhook.d.cts +497 -0
- package/dist/webhook.d.ts +497 -0
- package/dist/webhook.js +422 -0
- package/dist/webhook.js.map +1 -0
- package/dist/workflow.cjs +2 -2614
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +12 -3023
- package/dist/workflow.d.ts +12 -3023
- package/dist/workflow.js +2 -2535
- package/dist/workflow.js.map +1 -1
- package/package.json +46 -1
package/dist/hitl.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
// src/core/index.ts
|
|
2
|
+
function ok(value) {
|
|
3
|
+
return { ok: true, value };
|
|
4
|
+
}
|
|
5
|
+
function err(error, options) {
|
|
6
|
+
const cause = options?.cause;
|
|
7
|
+
return { ok: false, error, ...cause !== void 0 ? { cause } : {} };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/workflow/guards.ts
|
|
11
|
+
function isStepComplete(event) {
|
|
12
|
+
return event.type === "step_complete";
|
|
13
|
+
}
|
|
14
|
+
function isPendingApproval(error) {
|
|
15
|
+
return typeof error === "object" && error !== null && error.type === "PENDING_APPROVAL";
|
|
16
|
+
}
|
|
17
|
+
function isApprovalRejected(error) {
|
|
18
|
+
return typeof error === "object" && error !== null && error.type === "APPROVAL_REJECTED";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/workflow/resume-state.ts
|
|
22
|
+
function injectApproval(state, options) {
|
|
23
|
+
const newSteps = new Map(state.steps);
|
|
24
|
+
newSteps.set(options.stepKey, {
|
|
25
|
+
result: ok(options.value)
|
|
26
|
+
});
|
|
27
|
+
return { steps: newSteps };
|
|
28
|
+
}
|
|
29
|
+
function clearStep(state, stepKey) {
|
|
30
|
+
const newSteps = new Map(state.steps);
|
|
31
|
+
newSteps.delete(stepKey);
|
|
32
|
+
return { steps: newSteps };
|
|
33
|
+
}
|
|
34
|
+
function hasPendingApproval(state, stepKey) {
|
|
35
|
+
const entry = state.steps.get(stepKey);
|
|
36
|
+
if (!entry || entry.result.ok) return false;
|
|
37
|
+
return isPendingApproval(entry.result.error);
|
|
38
|
+
}
|
|
39
|
+
function getPendingApprovals(state) {
|
|
40
|
+
const pending = [];
|
|
41
|
+
for (const [key, entry] of state.steps) {
|
|
42
|
+
if (!entry.result.ok && isPendingApproval(entry.result.error)) {
|
|
43
|
+
pending.push(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return pending;
|
|
47
|
+
}
|
|
48
|
+
function createApprovalStateCollector() {
|
|
49
|
+
const steps = /* @__PURE__ */ new Map();
|
|
50
|
+
return {
|
|
51
|
+
handleEvent: (event) => {
|
|
52
|
+
if (isStepComplete(event)) {
|
|
53
|
+
steps.set(event.stepKey, { result: event.result, meta: event.meta });
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
getResumeState: () => ({ steps: new Map(steps) }),
|
|
57
|
+
clear: () => steps.clear(),
|
|
58
|
+
hasPendingApprovals: () => {
|
|
59
|
+
for (const entry of steps.values()) {
|
|
60
|
+
if (!entry.result.ok && isPendingApproval(entry.result.error)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
},
|
|
66
|
+
getPendingApprovals: () => {
|
|
67
|
+
const pending = [];
|
|
68
|
+
for (const [key, entry] of steps) {
|
|
69
|
+
if (!entry.result.ok && isPendingApproval(entry.result.error)) {
|
|
70
|
+
pending.push({ stepKey: key, error: entry.result.error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return pending;
|
|
74
|
+
},
|
|
75
|
+
injectApproval: (stepKey, value) => {
|
|
76
|
+
steps.set(stepKey, { result: ok(value) });
|
|
77
|
+
return { steps: new Map(steps) };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/workflow/hitl.ts
|
|
83
|
+
function pendingApproval(stepKey, options) {
|
|
84
|
+
return err({
|
|
85
|
+
type: "PENDING_APPROVAL",
|
|
86
|
+
stepKey,
|
|
87
|
+
reason: options?.reason,
|
|
88
|
+
metadata: options?.metadata
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function createApprovalStep(options) {
|
|
92
|
+
return async () => {
|
|
93
|
+
const result = await options.checkApproval();
|
|
94
|
+
switch (result.status) {
|
|
95
|
+
case "pending":
|
|
96
|
+
return err({
|
|
97
|
+
type: "PENDING_APPROVAL",
|
|
98
|
+
stepKey: options.key,
|
|
99
|
+
reason: options.pendingReason,
|
|
100
|
+
metadata: options.metadata
|
|
101
|
+
});
|
|
102
|
+
case "rejected":
|
|
103
|
+
return err({
|
|
104
|
+
type: "APPROVAL_REJECTED",
|
|
105
|
+
stepKey: options.key,
|
|
106
|
+
reason: result.reason
|
|
107
|
+
});
|
|
108
|
+
case "approved":
|
|
109
|
+
return ok(result.value);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function gatedStep(operation, options) {
|
|
114
|
+
return async (args) => {
|
|
115
|
+
const requiresApproval = typeof options.requiresApproval === "function" ? await options.requiresApproval(args) : options.requiresApproval;
|
|
116
|
+
if (!requiresApproval) {
|
|
117
|
+
return operation(args);
|
|
118
|
+
}
|
|
119
|
+
if (options.checkApproval) {
|
|
120
|
+
const approvalStatus = await options.checkApproval();
|
|
121
|
+
switch (approvalStatus.status) {
|
|
122
|
+
case "approved":
|
|
123
|
+
return operation(args);
|
|
124
|
+
case "rejected":
|
|
125
|
+
return err({
|
|
126
|
+
type: "APPROVAL_REJECTED",
|
|
127
|
+
stepKey: options.key,
|
|
128
|
+
reason: approvalStatus.reason
|
|
129
|
+
});
|
|
130
|
+
case "pending":
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const description = typeof options.description === "function" ? options.description(args) : options.description;
|
|
135
|
+
return err({
|
|
136
|
+
type: "PENDING_APPROVAL",
|
|
137
|
+
stepKey: options.key,
|
|
138
|
+
reason: description,
|
|
139
|
+
metadata: {
|
|
140
|
+
...options.metadata,
|
|
141
|
+
pendingArgs: args,
|
|
142
|
+
gatedOperation: true
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/hitl/index.ts
|
|
149
|
+
function createMemoryApprovalStore() {
|
|
150
|
+
const approvals = /* @__PURE__ */ new Map();
|
|
151
|
+
return {
|
|
152
|
+
async getApproval(key) {
|
|
153
|
+
const approval = approvals.get(key);
|
|
154
|
+
if (!approval) {
|
|
155
|
+
return { status: "pending" };
|
|
156
|
+
}
|
|
157
|
+
if (approval.expiresAt && Date.now() > approval.expiresAt) {
|
|
158
|
+
return { status: "expired", expiredAt: approval.expiresAt };
|
|
159
|
+
}
|
|
160
|
+
return approval;
|
|
161
|
+
},
|
|
162
|
+
async createApproval(key, options) {
|
|
163
|
+
approvals.set(key, {
|
|
164
|
+
status: "pending",
|
|
165
|
+
metadata: options?.metadata,
|
|
166
|
+
expiresAt: options?.expiresAt
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
async grantApproval(key, value, options) {
|
|
170
|
+
approvals.set(key, {
|
|
171
|
+
status: "approved",
|
|
172
|
+
value,
|
|
173
|
+
approvedBy: options?.approvedBy,
|
|
174
|
+
approvedAt: Date.now()
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
async rejectApproval(key, reason, options) {
|
|
178
|
+
approvals.set(key, {
|
|
179
|
+
status: "rejected",
|
|
180
|
+
reason,
|
|
181
|
+
rejectedBy: options?.rejectedBy,
|
|
182
|
+
rejectedAt: Date.now()
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
async editApproval(key, originalValue, editedValue, options) {
|
|
186
|
+
approvals.set(key, {
|
|
187
|
+
status: "edited",
|
|
188
|
+
originalValue,
|
|
189
|
+
editedValue,
|
|
190
|
+
editedBy: options?.editedBy,
|
|
191
|
+
editedAt: Date.now()
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
async cancelApproval(key) {
|
|
195
|
+
approvals.delete(key);
|
|
196
|
+
},
|
|
197
|
+
async listPending(options) {
|
|
198
|
+
const pending = [];
|
|
199
|
+
for (const [key, approval] of approvals) {
|
|
200
|
+
if (approval.status === "pending") {
|
|
201
|
+
if (!options?.prefix || key.startsWith(options.prefix)) {
|
|
202
|
+
pending.push(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return pending;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function createMemoryWorkflowStateStore() {
|
|
211
|
+
const states = /* @__PURE__ */ new Map();
|
|
212
|
+
return {
|
|
213
|
+
async save(state) {
|
|
214
|
+
states.set(state.runId, { ...state, updatedAt: Date.now() });
|
|
215
|
+
},
|
|
216
|
+
async load(runId) {
|
|
217
|
+
return states.get(runId);
|
|
218
|
+
},
|
|
219
|
+
async delete(runId) {
|
|
220
|
+
states.delete(runId);
|
|
221
|
+
},
|
|
222
|
+
async list(options) {
|
|
223
|
+
const results = [];
|
|
224
|
+
for (const [runId, state] of states) {
|
|
225
|
+
if (options?.workflowName && state.workflowName !== options.workflowName) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (options?.hasPendingApprovals !== void 0) {
|
|
229
|
+
const hasPending = state.pendingApprovals.length > 0;
|
|
230
|
+
if (options.hasPendingApprovals !== hasPending) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
results.push(runId);
|
|
235
|
+
}
|
|
236
|
+
return results;
|
|
237
|
+
},
|
|
238
|
+
async findByPendingApproval(approvalKey) {
|
|
239
|
+
const results = [];
|
|
240
|
+
for (const [runId, state] of states) {
|
|
241
|
+
if (state.pendingApprovals.includes(approvalKey)) {
|
|
242
|
+
results.push(runId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function createHITLOrchestrator(options) {
|
|
250
|
+
const {
|
|
251
|
+
approvalStore,
|
|
252
|
+
workflowStateStore,
|
|
253
|
+
defaultExpirationMs = 7 * 24 * 60 * 60 * 1e3,
|
|
254
|
+
// 7 days
|
|
255
|
+
logger = () => {
|
|
256
|
+
},
|
|
257
|
+
notificationChannel
|
|
258
|
+
} = options;
|
|
259
|
+
async function execute(workflowName, workflowFactory, workflowFn, input, opts) {
|
|
260
|
+
const runId = opts?.runId ?? crypto.randomUUID();
|
|
261
|
+
const collector = createApprovalStateCollector();
|
|
262
|
+
const workflow = workflowFactory({
|
|
263
|
+
onEvent: collector.handleEvent
|
|
264
|
+
});
|
|
265
|
+
const args = input;
|
|
266
|
+
const result = await workflow.run(
|
|
267
|
+
async (context) => workflowFn({ ...context, args })
|
|
268
|
+
);
|
|
269
|
+
const pendingApprovals = collector.getPendingApprovals().map((p) => p.stepKey);
|
|
270
|
+
if (pendingApprovals.length > 0) {
|
|
271
|
+
const state = {
|
|
272
|
+
runId,
|
|
273
|
+
workflowName,
|
|
274
|
+
resumeState: collector.getResumeState(),
|
|
275
|
+
pendingApprovals,
|
|
276
|
+
input,
|
|
277
|
+
metadata: opts?.metadata,
|
|
278
|
+
startedAt: Date.now(),
|
|
279
|
+
updatedAt: Date.now()
|
|
280
|
+
};
|
|
281
|
+
await workflowStateStore.save(state);
|
|
282
|
+
let reason;
|
|
283
|
+
let pendingMetadata;
|
|
284
|
+
if (!result.ok && isPendingApproval(result.error)) {
|
|
285
|
+
reason = result.error.reason;
|
|
286
|
+
pendingMetadata = result.error.metadata;
|
|
287
|
+
}
|
|
288
|
+
const expiresAt = Date.now() + defaultExpirationMs;
|
|
289
|
+
for (const key of pendingApprovals) {
|
|
290
|
+
await approvalStore.createApproval(key, {
|
|
291
|
+
metadata: { ...pendingMetadata, runId, workflowName },
|
|
292
|
+
expiresAt
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (notificationChannel) {
|
|
296
|
+
for (const key of pendingApprovals) {
|
|
297
|
+
try {
|
|
298
|
+
const pendingInfo = collector.getPendingApprovals().find((p) => p.stepKey === key);
|
|
299
|
+
await notificationChannel.onApprovalNeeded({
|
|
300
|
+
approvalKey: key,
|
|
301
|
+
runId,
|
|
302
|
+
workflowName,
|
|
303
|
+
reason,
|
|
304
|
+
metadata: { ...opts?.metadata, ...pendingMetadata },
|
|
305
|
+
expiresAt,
|
|
306
|
+
summary: reason ?? `Approval needed for ${key}`,
|
|
307
|
+
pendingArgs: pendingInfo?.error.metadata
|
|
308
|
+
});
|
|
309
|
+
} catch (notifyError) {
|
|
310
|
+
logger(`Failed to notify for approval ${key}: ${notifyError}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
logger(`Workflow ${runId} paused, waiting for: ${pendingApprovals.join(", ")}`);
|
|
315
|
+
return {
|
|
316
|
+
status: "paused",
|
|
317
|
+
runId,
|
|
318
|
+
pendingApprovals,
|
|
319
|
+
reason
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
logger(`Workflow ${runId} completed`);
|
|
323
|
+
return { status: "completed", result };
|
|
324
|
+
}
|
|
325
|
+
async function resume(runId, workflowFactory, workflowFn) {
|
|
326
|
+
const savedState = await workflowStateStore.load(runId);
|
|
327
|
+
if (!savedState) {
|
|
328
|
+
throw new Error(`Workflow run not found: ${runId}`);
|
|
329
|
+
}
|
|
330
|
+
for (const key of savedState.pendingApprovals) {
|
|
331
|
+
const status = await approvalStore.getApproval(key);
|
|
332
|
+
if (status.status === "pending") {
|
|
333
|
+
return {
|
|
334
|
+
status: "paused",
|
|
335
|
+
runId,
|
|
336
|
+
pendingApprovals: savedState.pendingApprovals
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (status.status === "rejected") {
|
|
340
|
+
}
|
|
341
|
+
if (status.status === "expired") {
|
|
342
|
+
throw new Error(`Approval ${key} has expired`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
let resumeState = savedState.resumeState;
|
|
346
|
+
for (const key of savedState.pendingApprovals) {
|
|
347
|
+
const status = await approvalStore.getApproval(key);
|
|
348
|
+
if (status.status === "approved") {
|
|
349
|
+
resumeState = injectApproval(resumeState, {
|
|
350
|
+
stepKey: key,
|
|
351
|
+
value: status.value
|
|
352
|
+
});
|
|
353
|
+
} else if (status.status === "edited") {
|
|
354
|
+
resumeState = injectApproval(resumeState, {
|
|
355
|
+
stepKey: key,
|
|
356
|
+
value: status.editedValue
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const collector = createApprovalStateCollector();
|
|
361
|
+
const workflow = workflowFactory({
|
|
362
|
+
resumeState,
|
|
363
|
+
onEvent: collector.handleEvent
|
|
364
|
+
});
|
|
365
|
+
const args = savedState.input;
|
|
366
|
+
const result = await workflow.run(
|
|
367
|
+
async (context) => workflowFn({ ...context, args })
|
|
368
|
+
);
|
|
369
|
+
const newPendingApprovals = collector.getPendingApprovals().map((p) => p.stepKey);
|
|
370
|
+
if (newPendingApprovals.length > 0) {
|
|
371
|
+
const updatedState = {
|
|
372
|
+
...savedState,
|
|
373
|
+
resumeState: collector.getResumeState(),
|
|
374
|
+
pendingApprovals: newPendingApprovals,
|
|
375
|
+
updatedAt: Date.now()
|
|
376
|
+
};
|
|
377
|
+
await workflowStateStore.save(updatedState);
|
|
378
|
+
for (const key of newPendingApprovals) {
|
|
379
|
+
await approvalStore.createApproval(key, {
|
|
380
|
+
metadata: { runId, workflowName: savedState.workflowName },
|
|
381
|
+
expiresAt: Date.now() + defaultExpirationMs
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
logger(`Workflow ${runId} paused again, waiting for: ${newPendingApprovals.join(", ")}`);
|
|
385
|
+
let reason;
|
|
386
|
+
if (!result.ok && isPendingApproval(result.error)) {
|
|
387
|
+
reason = result.error.reason;
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
status: "paused",
|
|
391
|
+
runId,
|
|
392
|
+
pendingApprovals: newPendingApprovals,
|
|
393
|
+
reason
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
await workflowStateStore.delete(runId);
|
|
397
|
+
logger(`Workflow ${runId} resumed and completed`);
|
|
398
|
+
return { status: "resumed", runId, result };
|
|
399
|
+
}
|
|
400
|
+
async function grantApprovalFn(approvalKey, value, opts) {
|
|
401
|
+
await approvalStore.grantApproval(approvalKey, value, {
|
|
402
|
+
approvedBy: opts?.approvedBy
|
|
403
|
+
});
|
|
404
|
+
const grantedAt = Date.now();
|
|
405
|
+
const resumedWorkflows = [];
|
|
406
|
+
if (opts?.autoResume !== false) {
|
|
407
|
+
const waitingWorkflows = await workflowStateStore.findByPendingApproval(approvalKey);
|
|
408
|
+
for (const runId of waitingWorkflows) {
|
|
409
|
+
resumedWorkflows.push(runId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (notificationChannel?.onApprovalResolved) {
|
|
413
|
+
await notificationChannel.onApprovalResolved({
|
|
414
|
+
approvalKey,
|
|
415
|
+
action: "approved",
|
|
416
|
+
actorId: opts?.approvedBy,
|
|
417
|
+
resolvedAt: grantedAt,
|
|
418
|
+
value
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
logger(`Approval ${approvalKey} granted by ${opts?.approvedBy ?? "unknown"}`);
|
|
422
|
+
return { grantedAt, resumedWorkflows };
|
|
423
|
+
}
|
|
424
|
+
async function rejectApprovalFn(approvalKey, reason, opts) {
|
|
425
|
+
await approvalStore.rejectApproval(approvalKey, reason, {
|
|
426
|
+
rejectedBy: opts?.rejectedBy
|
|
427
|
+
});
|
|
428
|
+
if (notificationChannel?.onApprovalResolved) {
|
|
429
|
+
await notificationChannel.onApprovalResolved({
|
|
430
|
+
approvalKey,
|
|
431
|
+
action: "rejected",
|
|
432
|
+
actorId: opts?.rejectedBy,
|
|
433
|
+
resolvedAt: Date.now(),
|
|
434
|
+
reason
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
logger(`Approval ${approvalKey} rejected: ${reason}`);
|
|
438
|
+
}
|
|
439
|
+
async function editApprovalFn(approvalKey, originalValue, editedValue, opts) {
|
|
440
|
+
await approvalStore.editApproval(approvalKey, originalValue, editedValue, {
|
|
441
|
+
editedBy: opts?.editedBy
|
|
442
|
+
});
|
|
443
|
+
const editedAt = Date.now();
|
|
444
|
+
if (notificationChannel?.onApprovalResolved) {
|
|
445
|
+
await notificationChannel.onApprovalResolved({
|
|
446
|
+
approvalKey,
|
|
447
|
+
action: "edited",
|
|
448
|
+
actorId: opts?.editedBy,
|
|
449
|
+
resolvedAt: editedAt,
|
|
450
|
+
value: editedValue,
|
|
451
|
+
originalValue
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
logger(`Approval ${approvalKey} edited by ${opts?.editedBy ?? "unknown"}`);
|
|
455
|
+
return { editedAt };
|
|
456
|
+
}
|
|
457
|
+
async function pollApproval(approvalKey, opts) {
|
|
458
|
+
const {
|
|
459
|
+
intervalMs = 1e3,
|
|
460
|
+
maxPolls,
|
|
461
|
+
timeoutMs,
|
|
462
|
+
onPollStart,
|
|
463
|
+
onPollComplete
|
|
464
|
+
} = opts ?? {};
|
|
465
|
+
const startTime = Date.now();
|
|
466
|
+
let pollCount = 0;
|
|
467
|
+
while (true) {
|
|
468
|
+
onPollStart?.();
|
|
469
|
+
const status = await approvalStore.getApproval(approvalKey);
|
|
470
|
+
onPollComplete?.(status);
|
|
471
|
+
if (status.status !== "pending") {
|
|
472
|
+
return status;
|
|
473
|
+
}
|
|
474
|
+
pollCount++;
|
|
475
|
+
if (maxPolls !== void 0 && pollCount >= maxPolls) {
|
|
476
|
+
return { status: "pending" };
|
|
477
|
+
}
|
|
478
|
+
if (timeoutMs !== void 0 && Date.now() - startTime >= timeoutMs) {
|
|
479
|
+
return { status: "pending" };
|
|
480
|
+
}
|
|
481
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function getWorkflowStatus(runId) {
|
|
485
|
+
return workflowStateStore.load(runId);
|
|
486
|
+
}
|
|
487
|
+
async function listPendingWorkflows(workflowName) {
|
|
488
|
+
return workflowStateStore.list({
|
|
489
|
+
workflowName,
|
|
490
|
+
hasPendingApprovals: true
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async function cleanup(maxAgeMs) {
|
|
494
|
+
const allWorkflows = await workflowStateStore.list();
|
|
495
|
+
let cleaned = 0;
|
|
496
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
497
|
+
for (const runId of allWorkflows) {
|
|
498
|
+
const state = await workflowStateStore.load(runId);
|
|
499
|
+
if (state && state.updatedAt < cutoff && state.pendingApprovals.length === 0) {
|
|
500
|
+
await workflowStateStore.delete(runId);
|
|
501
|
+
cleaned++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
logger(`Cleaned up ${cleaned} old workflow states`);
|
|
505
|
+
return cleaned;
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
execute,
|
|
509
|
+
resume,
|
|
510
|
+
grantApproval: grantApprovalFn,
|
|
511
|
+
rejectApproval: rejectApprovalFn,
|
|
512
|
+
editApproval: editApprovalFn,
|
|
513
|
+
pollApproval,
|
|
514
|
+
getWorkflowStatus,
|
|
515
|
+
listPendingWorkflows,
|
|
516
|
+
cleanup
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function createApprovalWebhookHandler(store) {
|
|
520
|
+
return async (request) => {
|
|
521
|
+
const { key, action, value, originalValue, editedValue, reason, actorId } = request;
|
|
522
|
+
try {
|
|
523
|
+
switch (action) {
|
|
524
|
+
case "approve":
|
|
525
|
+
await store.grantApproval(key, value, { approvedBy: actorId });
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
message: `Approval ${key} granted`,
|
|
529
|
+
data: { key, action, timestamp: Date.now() }
|
|
530
|
+
};
|
|
531
|
+
case "reject":
|
|
532
|
+
if (!reason) {
|
|
533
|
+
return { success: false, message: "Reason is required for rejection" };
|
|
534
|
+
}
|
|
535
|
+
await store.rejectApproval(key, reason, { rejectedBy: actorId });
|
|
536
|
+
return {
|
|
537
|
+
success: true,
|
|
538
|
+
message: `Approval ${key} rejected`,
|
|
539
|
+
data: { key, action, timestamp: Date.now() }
|
|
540
|
+
};
|
|
541
|
+
case "edit":
|
|
542
|
+
if (originalValue === void 0 || editedValue === void 0) {
|
|
543
|
+
return { success: false, message: "Both originalValue and editedValue are required for edit" };
|
|
544
|
+
}
|
|
545
|
+
await store.editApproval(key, originalValue, editedValue, { editedBy: actorId });
|
|
546
|
+
return {
|
|
547
|
+
success: true,
|
|
548
|
+
message: `Approval ${key} edited`,
|
|
549
|
+
data: { key, action, timestamp: Date.now() }
|
|
550
|
+
};
|
|
551
|
+
case "cancel":
|
|
552
|
+
await store.cancelApproval(key);
|
|
553
|
+
return {
|
|
554
|
+
success: true,
|
|
555
|
+
message: `Approval ${key} cancelled`,
|
|
556
|
+
data: { key, action, timestamp: Date.now() }
|
|
557
|
+
};
|
|
558
|
+
default:
|
|
559
|
+
return { success: false, message: `Unknown action: ${action}` };
|
|
560
|
+
}
|
|
561
|
+
} catch (error) {
|
|
562
|
+
return {
|
|
563
|
+
success: false,
|
|
564
|
+
message: error instanceof Error ? error.message : String(error)
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function createApprovalChecker(store) {
|
|
570
|
+
return (key) => async () => {
|
|
571
|
+
const status = await store.getApproval(key);
|
|
572
|
+
switch (status.status) {
|
|
573
|
+
case "pending":
|
|
574
|
+
return { status: "pending" };
|
|
575
|
+
case "approved":
|
|
576
|
+
return { status: "approved", value: status.value };
|
|
577
|
+
case "edited":
|
|
578
|
+
return { status: "approved", value: status.editedValue };
|
|
579
|
+
case "rejected":
|
|
580
|
+
return { status: "rejected", reason: status.reason };
|
|
581
|
+
case "expired":
|
|
582
|
+
return { status: "rejected", reason: "Approval request expired" };
|
|
583
|
+
default:
|
|
584
|
+
return { status: "pending" };
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
export {
|
|
589
|
+
clearStep,
|
|
590
|
+
createApprovalChecker,
|
|
591
|
+
createApprovalStateCollector,
|
|
592
|
+
createApprovalStep,
|
|
593
|
+
createApprovalWebhookHandler,
|
|
594
|
+
createHITLOrchestrator,
|
|
595
|
+
createMemoryApprovalStore,
|
|
596
|
+
createMemoryWorkflowStateStore,
|
|
597
|
+
gatedStep,
|
|
598
|
+
getPendingApprovals,
|
|
599
|
+
hasPendingApproval,
|
|
600
|
+
injectApproval,
|
|
601
|
+
isApprovalRejected,
|
|
602
|
+
isPendingApproval,
|
|
603
|
+
pendingApproval
|
|
604
|
+
};
|
|
605
|
+
//# sourceMappingURL=hitl.js.map
|