drej 0.4.0 → 0.5.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/index.js ADDED
@@ -0,0 +1,1096 @@
1
+ // ../../core/src/ledger.ts
2
+ var LedgerEvent = /* @__PURE__ */ ((LedgerEvent2) => {
3
+ LedgerEvent2["RunStarted"] = "run_started";
4
+ LedgerEvent2["StepStart"] = "step_start";
5
+ LedgerEvent2["StepComplete"] = "step_complete";
6
+ LedgerEvent2["StepFailed"] = "step_failed";
7
+ LedgerEvent2["StepRolledBack"] = "step_rolled_back";
8
+ LedgerEvent2["WorkflowComplete"] = "workflow_complete";
9
+ LedgerEvent2["WorkflowFailed"] = "workflow_failed";
10
+ LedgerEvent2["Checkpoint"] = "checkpoint";
11
+ LedgerEvent2["ExecEvent"] = "exec_event";
12
+ LedgerEvent2["Snapshot"] = "snapshot";
13
+ return LedgerEvent2;
14
+ })(LedgerEvent || {});
15
+
16
+ // ../../core/src/logger.ts
17
+ var noopLogger = {
18
+ debug: () => {
19
+ },
20
+ info: () => {
21
+ },
22
+ warn: () => {
23
+ },
24
+ error: () => {
25
+ }
26
+ };
27
+
28
+ // ../../core/src/workflow.ts
29
+ var Workflow = class _Workflow {
30
+ constructor(name, runId, steps, deps) {
31
+ this.steps = steps;
32
+ this.deps = deps;
33
+ this.name = name;
34
+ this.runId = runId;
35
+ this.log = deps.logger ?? noopLogger;
36
+ }
37
+ steps;
38
+ deps;
39
+ name;
40
+ runId;
41
+ _status = "idle";
42
+ completedSteps = /* @__PURE__ */ new Map();
43
+ log;
44
+ get status() {
45
+ return this._status;
46
+ }
47
+ async callHook(name, info) {
48
+ const hook = this.deps.hooks?.[name];
49
+ if (!hook) return;
50
+ try {
51
+ await hook(info);
52
+ } catch (err) {
53
+ this.log.warn(`hook ${name} threw`, { error: String(err) });
54
+ }
55
+ }
56
+ async run(input, startFromStep = 0) {
57
+ this._status = "running";
58
+ this.log.info("workflow started", { workflowName: this.name, runId: this.runId, startFromStep, totalSteps: this.steps.length });
59
+ let current = startFromStep > 0 ? this.completedSteps.get(startFromStep - 1)?.output ?? input : input;
60
+ for (let i = startFromStep; i < this.steps.length; i++) {
61
+ const step = this.steps[i];
62
+ const ctx = this.makeContext(i);
63
+ this.log.debug("step starting", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id });
64
+ await this.callHook("onStepStart", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id });
65
+ await ctx.emit({ ts: Date.now(), workflowName: this.name, runId: this.runId, stepIndex: i, event: "step_start" /* StepStart */ });
66
+ try {
67
+ const output = await step.run(current, ctx);
68
+ this.completedSteps.set(i, { output, completedAt: Date.now() });
69
+ current = output;
70
+ this.log.debug("step complete", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id });
71
+ await this.callHook("onStepComplete", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id, output });
72
+ await ctx.emit({
73
+ ts: Date.now(),
74
+ workflowName: this.name,
75
+ runId: this.runId,
76
+ stepIndex: i,
77
+ event: "step_complete" /* StepComplete */,
78
+ payload: output
79
+ });
80
+ await this.deps.adapter.append({
81
+ ts: Date.now(),
82
+ workflowName: this.name,
83
+ runId: this.runId,
84
+ stepIndex: i + 1,
85
+ event: "checkpoint" /* Checkpoint */,
86
+ payload: this.snapshot()
87
+ });
88
+ } catch (err) {
89
+ this._status = "failed";
90
+ const error = err instanceof Error ? err : new Error(String(err));
91
+ this.log.error("step failed", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id, error: error.message });
92
+ await this.callHook("onStepFailed", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id, error });
93
+ await ctx.emit({
94
+ ts: Date.now(),
95
+ workflowName: this.name,
96
+ runId: this.runId,
97
+ stepIndex: i,
98
+ event: "step_failed" /* StepFailed */,
99
+ error: error.message
100
+ });
101
+ await this.callHook("onWorkflowFailed", { workflowName: this.name, runId: this.runId, error });
102
+ throw error;
103
+ }
104
+ }
105
+ this._status = "completed";
106
+ this.log.info("workflow complete", { workflowName: this.name, runId: this.runId });
107
+ await this.deps.adapter.append({
108
+ ts: Date.now(),
109
+ workflowName: this.name,
110
+ runId: this.runId,
111
+ stepIndex: -1,
112
+ event: "workflow_complete" /* WorkflowComplete */
113
+ });
114
+ await this.callHook("onWorkflowComplete", { workflowName: this.name, runId: this.runId, output: current });
115
+ return current;
116
+ }
117
+ async rollback(toStep = 0) {
118
+ this.log.info("rolling back workflow", { workflowName: this.name, runId: this.runId });
119
+ const stepsToUndo = [...this.completedSteps.entries()].filter(([i]) => i >= toStep).sort(([a], [b]) => b - a);
120
+ for (const [i, result] of stepsToUndo) {
121
+ const step = this.steps[i];
122
+ if (step.rollback) {
123
+ const ctx = this.makeContext(i);
124
+ this.log.debug("rolling back step", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id });
125
+ await step.rollback(result.output, ctx);
126
+ await this.callHook("onStepRolledBack", { workflowName: this.name, runId: this.runId, stepIndex: i, stepId: step.id });
127
+ await ctx.emit({
128
+ ts: Date.now(),
129
+ workflowName: this.name,
130
+ runId: this.runId,
131
+ stepIndex: i,
132
+ event: "step_rolled_back" /* StepRolledBack */
133
+ });
134
+ this.completedSteps.delete(i);
135
+ }
136
+ }
137
+ this._status = "rolled_back";
138
+ this.log.info("workflow rolled back", { workflowName: this.name, runId: this.runId });
139
+ await this.deps.adapter.append({
140
+ ts: Date.now(),
141
+ workflowName: this.name,
142
+ runId: this.runId,
143
+ stepIndex: -1,
144
+ event: "workflow_failed" /* WorkflowFailed */,
145
+ error: "rolled_back"
146
+ });
147
+ await this.callHook("onWorkflowFailed", { workflowName: this.name, runId: this.runId, error: new Error("rolled_back") });
148
+ }
149
+ snapshot() {
150
+ return {
151
+ workflowName: this.name,
152
+ runId: this.runId,
153
+ stepIndex: this.completedSteps.size,
154
+ completedSteps: Object.fromEntries(this.completedSteps),
155
+ timestamp: Date.now()
156
+ };
157
+ }
158
+ static async resumeFromLedger(workflowName, runId, steps, deps) {
159
+ const wf = new _Workflow(workflowName, runId, steps, deps);
160
+ const entry = await deps.adapter.lastCheckpoint(workflowName, runId);
161
+ if (entry?.payload) {
162
+ const cp = entry.payload;
163
+ for (const [idx, result] of Object.entries(cp.completedSteps)) {
164
+ wf.completedSteps.set(Number(idx), result);
165
+ }
166
+ const lastOutput = cp.completedSteps[cp.stepIndex - 1]?.output ?? {};
167
+ deps.logger?.info("resuming workflow from checkpoint", { workflowName, runId, nextStep: cp.stepIndex });
168
+ return { workflow: wf, nextStep: cp.stepIndex, lastOutput };
169
+ }
170
+ return { workflow: wf, nextStep: 0, lastOutput: {} };
171
+ }
172
+ makeContext(stepIndex) {
173
+ return {
174
+ workflowName: this.name,
175
+ runId: this.runId,
176
+ stepIndex,
177
+ control: this.deps.control,
178
+ resolveExec: this.deps.resolveExec,
179
+ emit: (entry) => this.deps.adapter.append(entry)
180
+ };
181
+ }
182
+ };
183
+
184
+ // ../../opensandbox/src/control.ts
185
+ function flattenSnapshot(raw) {
186
+ return { id: raw.id, sandboxId: raw.sandboxId, state: raw.status.state, createdAt: raw.createdAt };
187
+ }
188
+ var OpenSandboxError = class extends Error {
189
+ constructor(message, status) {
190
+ super(message);
191
+ this.status = status;
192
+ this.name = "OpenSandboxError";
193
+ }
194
+ status;
195
+ };
196
+ var ControlClient = class {
197
+ baseUrl;
198
+ apiKey;
199
+ constructor(options) {
200
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
201
+ this.apiKey = options.apiKey;
202
+ }
203
+ async request(method, path, body) {
204
+ const res = await fetch(`${this.baseUrl}${path}`, {
205
+ method,
206
+ headers: {
207
+ "OPEN-SANDBOX-API-KEY": this.apiKey,
208
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
209
+ },
210
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
211
+ });
212
+ if (!res.ok) {
213
+ const text = await res.text().catch(() => "");
214
+ throw new OpenSandboxError(text || "OpenSandbox API error", res.status);
215
+ }
216
+ if (res.status === 204) return void 0;
217
+ return res.json();
218
+ }
219
+ createSandbox(options) {
220
+ return this.request("POST", "/v1/sandboxes", options);
221
+ }
222
+ listSandboxes(options = {}) {
223
+ const params = new URLSearchParams();
224
+ if (options.state) params.set("state", options.state);
225
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
226
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
227
+ const qs = params.toString();
228
+ return this.request("GET", `/v1/sandboxes${qs ? `?${qs}` : ""}`);
229
+ }
230
+ getSandbox(id) {
231
+ return this.request("GET", `/v1/sandboxes/${id}`);
232
+ }
233
+ deleteSandbox(id) {
234
+ return this.request("DELETE", `/v1/sandboxes/${id}`);
235
+ }
236
+ pauseSandbox(id) {
237
+ return this.request("POST", `/v1/sandboxes/${id}/pause`);
238
+ }
239
+ resumeSandbox(id) {
240
+ return this.request("POST", `/v1/sandboxes/${id}/resume`);
241
+ }
242
+ renewExpiration(id) {
243
+ return this.request("POST", `/v1/sandboxes/${id}/renew-expiration`);
244
+ }
245
+ // Returns { endpoint, headers: { "X-EXECD-ACCESS-TOKEN": "..." } }
246
+ getEndpoint(sandboxId, port) {
247
+ return this.request("GET", `/v1/sandboxes/${sandboxId}/endpoints/${port}`);
248
+ }
249
+ getDiagnosticLogs(sandboxId) {
250
+ return this.request("GET", `/v1/sandboxes/${sandboxId}/diagnostics/logs`);
251
+ }
252
+ getDiagnosticEvents(sandboxId) {
253
+ return this.request("GET", `/v1/sandboxes/${sandboxId}/diagnostics/events`);
254
+ }
255
+ async createSnapshot(sandboxId) {
256
+ const raw = await this.request("POST", `/v1/sandboxes/${sandboxId}/snapshots`);
257
+ return flattenSnapshot(raw);
258
+ }
259
+ listSnapshots(options = {}) {
260
+ const params = new URLSearchParams();
261
+ if (options.sandboxId) params.set("sandboxId", options.sandboxId);
262
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
263
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
264
+ const qs = params.toString();
265
+ return this.request("GET", `/v1/snapshots${qs ? `?${qs}` : ""}`);
266
+ }
267
+ async getSnapshot(id) {
268
+ const raw = await this.request("GET", `/v1/snapshots/${id}`);
269
+ return flattenSnapshot(raw);
270
+ }
271
+ deleteSnapshot(id) {
272
+ return this.request("DELETE", `/v1/snapshots/${id}`);
273
+ }
274
+ };
275
+
276
+ // ../../opensandbox/src/exec.ts
277
+ async function* parseSSE(stream) {
278
+ const reader = stream.getReader();
279
+ const decoder = new TextDecoder();
280
+ let buffer = "";
281
+ try {
282
+ while (true) {
283
+ const { done, value } = await reader.read();
284
+ if (done) break;
285
+ buffer += decoder.decode(value, { stream: true });
286
+ const blocks = buffer.split("\n\n");
287
+ buffer = blocks.pop() ?? "";
288
+ for (const block of blocks) {
289
+ const trimmed = block.trim();
290
+ if (!trimmed) continue;
291
+ if (trimmed.startsWith("{")) {
292
+ try {
293
+ yield JSON.parse(trimmed);
294
+ } catch {
295
+ }
296
+ } else {
297
+ let type;
298
+ let data;
299
+ for (const line of block.split("\n")) {
300
+ if (line.startsWith("event:")) type = line.slice(6).trim();
301
+ else if (line.startsWith("data:")) data = line.slice(5).trim();
302
+ }
303
+ if (data !== void 0) {
304
+ yield { type: type ?? "message", ...JSON.parse(data) };
305
+ }
306
+ }
307
+ }
308
+ }
309
+ } finally {
310
+ reader.releaseLock();
311
+ }
312
+ }
313
+ var ExecClient = class {
314
+ baseUrl;
315
+ accessToken;
316
+ constructor(options) {
317
+ this.baseUrl = (options.baseUrl ?? "http://localhost:44772").replace(/\/$/, "");
318
+ this.accessToken = options.accessToken;
319
+ }
320
+ get authHeader() {
321
+ return { "X-EXECD-ACCESS-TOKEN": this.accessToken };
322
+ }
323
+ async request(method, path, body) {
324
+ const res = await fetch(`${this.baseUrl}${path}`, {
325
+ method,
326
+ headers: {
327
+ ...this.authHeader,
328
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
329
+ },
330
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
331
+ });
332
+ if (!res.ok) {
333
+ const text = await res.text().catch(() => "");
334
+ throw new Error(text || `execd error ${res.status}`);
335
+ }
336
+ if (res.status === 204) return void 0;
337
+ return res.json();
338
+ }
339
+ async *streamRequest(method, path, body) {
340
+ const res = await fetch(`${this.baseUrl}${path}`, {
341
+ method,
342
+ headers: {
343
+ ...this.authHeader,
344
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
345
+ },
346
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
347
+ });
348
+ if (!res.ok) {
349
+ const text = await res.text().catch(() => "");
350
+ throw new Error(text || `execd error ${res.status}`);
351
+ }
352
+ if (!res.body) return;
353
+ yield* parseSSE(res.body);
354
+ }
355
+ ping() {
356
+ return this.request("GET", "/ping");
357
+ }
358
+ listContexts(language) {
359
+ const qs = language ? `?language=${encodeURIComponent(language)}` : "";
360
+ return this.request("GET", `/code/contexts${qs}`);
361
+ }
362
+ clearContexts(language) {
363
+ const qs = language ? `?language=${encodeURIComponent(language)}` : "";
364
+ return this.request("DELETE", `/code/contexts${qs}`);
365
+ }
366
+ deleteContext(contextId) {
367
+ return this.request("DELETE", `/code/contexts/${contextId}`);
368
+ }
369
+ createContext(language) {
370
+ return this.request("POST", "/code/context", { language });
371
+ }
372
+ async *executeCode(options) {
373
+ yield* this.streamRequest("POST", "/code", options);
374
+ }
375
+ interruptCode() {
376
+ return this.request("DELETE", "/code");
377
+ }
378
+ async *executeCommand(options) {
379
+ yield* this.streamRequest("POST", "/command", options);
380
+ }
381
+ interruptCommand() {
382
+ return this.request("DELETE", "/command");
383
+ }
384
+ getCommandStatus(session) {
385
+ return this.request("GET", `/command/status/${session}`);
386
+ }
387
+ getCommandOutput(session) {
388
+ return this.request("GET", `/command/output/${session}`);
389
+ }
390
+ getFileInfo(path) {
391
+ return this.request("GET", `/files/info?path=${encodeURIComponent(path)}`);
392
+ }
393
+ deleteFile(path) {
394
+ return this.request("DELETE", `/files?path=${encodeURIComponent(path)}`);
395
+ }
396
+ setPermissions(path, mode) {
397
+ return this.request("POST", "/files/permissions", { path, mode });
398
+ }
399
+ moveFile(from, to) {
400
+ return this.request("POST", "/files/mv", { from, to });
401
+ }
402
+ searchFiles(pattern, dir) {
403
+ const params = new URLSearchParams({ pattern });
404
+ if (dir) params.set("dir", dir);
405
+ return this.request("GET", `/files/search?${params}`);
406
+ }
407
+ replaceInFiles(replacements) {
408
+ return this.request("POST", "/files/replace", { replacements });
409
+ }
410
+ async uploadFile(path, content) {
411
+ const formData = new FormData();
412
+ formData.append("metadata", new File([JSON.stringify({ path })], "metadata.json", { type: "application/json" }));
413
+ formData.append("file", new File([content], path.split("/").pop() ?? "file", { type: "application/octet-stream" }));
414
+ const res = await fetch(`${this.baseUrl}/files/upload`, {
415
+ method: "POST",
416
+ headers: this.authHeader,
417
+ body: formData
418
+ });
419
+ if (!res.ok) {
420
+ const text = await res.text().catch(() => "");
421
+ throw new Error(text || `execd error ${res.status}`);
422
+ }
423
+ }
424
+ async downloadFile(path) {
425
+ const res = await fetch(`${this.baseUrl}/files/download?path=${encodeURIComponent(path)}`, {
426
+ headers: this.authHeader
427
+ });
428
+ if (!res.ok) throw new Error(`execd error ${res.status}`);
429
+ if (!res.body) throw new Error("empty response body");
430
+ return res.body;
431
+ }
432
+ listDirectory(path, depth) {
433
+ const params = new URLSearchParams({ path });
434
+ if (depth !== void 0) params.set("depth", String(depth));
435
+ return this.request("GET", `/directories/list?${params}`);
436
+ }
437
+ createDirectory(path) {
438
+ return this.request("POST", "/directories", { path });
439
+ }
440
+ deleteDirectory(path) {
441
+ return this.request("DELETE", `/directories?path=${encodeURIComponent(path)}`);
442
+ }
443
+ getMetrics() {
444
+ return this.request("GET", "/metrics");
445
+ }
446
+ async *watchMetrics() {
447
+ yield* this.streamRequest("GET", "/metrics/watch");
448
+ }
449
+ };
450
+
451
+ // ../../core/src/steps.ts
452
+ async function resolveExecClient(control, sandboxId, retries = 15, delayMs = 1e3) {
453
+ const ep = await control.getEndpoint(sandboxId, 44772);
454
+ const baseUrl = ep.endpoint.startsWith("http") ? ep.endpoint : `http://${ep.endpoint}`;
455
+ const token = ep.headers?.["X-EXECD-ACCESS-TOKEN"] ?? "";
456
+ const client = new ExecClient({ baseUrl, accessToken: token });
457
+ for (let attempt = 0; attempt <= retries; attempt++) {
458
+ try {
459
+ await client.listContexts();
460
+ return client;
461
+ } catch {
462
+ if (attempt === retries) throw new Error(`execd not ready after ${retries}s for sandbox ${sandboxId}`);
463
+ await new Promise((r) => setTimeout(r, delayMs));
464
+ }
465
+ }
466
+ throw new Error("unreachable");
467
+ }
468
+ function shouldSnapshot(config, stepIndex) {
469
+ if (config.afterSteps?.includes(stepIndex)) return true;
470
+ if (config.everyNSteps && (stepIndex + 1) % config.everyNSteps === 0) return true;
471
+ return false;
472
+ }
473
+ async function waitForSnapshot(control, snapshotId, timeoutMs = 12e4) {
474
+ const deadline = Date.now() + timeoutMs;
475
+ while (Date.now() < deadline) {
476
+ const snap = await control.getSnapshot(snapshotId);
477
+ if (snap.state === "Ready") return;
478
+ if (snap.state === "Failed") throw new Error(`Snapshot ${snapshotId} failed`);
479
+ await new Promise((r) => setTimeout(r, 2e3));
480
+ }
481
+ throw new Error(`Snapshot ${snapshotId} did not become ready within ${timeoutMs}ms`);
482
+ }
483
+ function getPath(obj, path) {
484
+ return path.split(".").reduce((cur, key) => {
485
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
486
+ return cur[key];
487
+ }, obj);
488
+ }
489
+ function interpolate(template, state) {
490
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
491
+ const val = state[key];
492
+ return val !== void 0 ? String(val) : `{{${key}}}`;
493
+ });
494
+ }
495
+ function evaluate(predicate, state) {
496
+ switch (predicate.op) {
497
+ case "eq":
498
+ return getPath(state, predicate.field) === predicate.value;
499
+ case "neq":
500
+ return getPath(state, predicate.field) !== predicate.value;
501
+ case "gt":
502
+ return Number(getPath(state, predicate.field)) > predicate.value;
503
+ case "lt":
504
+ return Number(getPath(state, predicate.field)) < predicate.value;
505
+ case "gte":
506
+ return Number(getPath(state, predicate.field)) >= predicate.value;
507
+ case "lte":
508
+ return Number(getPath(state, predicate.field)) <= predicate.value;
509
+ case "exists":
510
+ return getPath(state, predicate.field) !== void 0;
511
+ case "not_exists":
512
+ return getPath(state, predicate.field) === void 0;
513
+ case "and":
514
+ return predicate.predicates.every((p) => evaluate(p, state));
515
+ case "or":
516
+ return predicate.predicates.some((p) => evaluate(p, state));
517
+ }
518
+ }
519
+ function buildStep(def) {
520
+ switch (def.type) {
521
+ case "create_sandbox":
522
+ return {
523
+ id: "create_sandbox",
524
+ async run(input, ctx) {
525
+ const state = input ?? {};
526
+ const sb = await ctx.control.createSandbox({
527
+ image: def.image,
528
+ snapshotId: def.snapshotId,
529
+ timeout: def.timeout,
530
+ entrypoint: def.entrypoint,
531
+ env: def.env,
532
+ metadata: def.metadata,
533
+ resourceLimits: def.resourceLimits
534
+ });
535
+ const deadline = Date.now() + 12e4;
536
+ while (Date.now() < deadline) {
537
+ const s = await ctx.control.getSandbox(sb.id);
538
+ if (s.status.state === "Running") break;
539
+ if (s.status.state === "Failed" || s.status.state === "Terminated") {
540
+ throw new Error(`Sandbox ${sb.id} entered state ${s.status.state}: ${s.status.message ?? ""}`);
541
+ }
542
+ await new Promise((r) => setTimeout(r, 1e3));
543
+ }
544
+ return { ...state, sandboxId: sb.id };
545
+ },
546
+ async rollback(output, ctx) {
547
+ const state = output;
548
+ if (state.sandboxId) await ctx.control.deleteSandbox(state.sandboxId);
549
+ }
550
+ };
551
+ case "exec_code":
552
+ return {
553
+ id: "exec_code",
554
+ async run(input, ctx) {
555
+ const state = input ?? {};
556
+ if (!state.sandboxId) throw new Error("exec_code requires sandboxId in workflow state");
557
+ const exec = await ctx.resolveExec(state.sandboxId);
558
+ const events = [];
559
+ for await (const ev of exec.executeCode({ code: def.code, context: def.context })) {
560
+ await ctx.emit({
561
+ ts: Date.now(),
562
+ workflowName: ctx.workflowName,
563
+ runId: ctx.runId,
564
+ stepIndex: ctx.stepIndex,
565
+ event: "exec_event" /* ExecEvent */,
566
+ payload: ev
567
+ });
568
+ events.push(ev);
569
+ }
570
+ return { ...state, codeEvents: events };
571
+ }
572
+ };
573
+ case "exec_command":
574
+ return {
575
+ id: "exec_command",
576
+ async run(input, ctx) {
577
+ const state = input ?? {};
578
+ if (!state.sandboxId) throw new Error("exec_command requires sandboxId in workflow state");
579
+ const exec = await ctx.resolveExec(state.sandboxId);
580
+ const raw = interpolate(def.command, state);
581
+ const command = `echo ${Buffer.from(raw).toString("base64")} | base64 -d | bash`;
582
+ const events = [];
583
+ for await (const ev of exec.executeCommand({ command, cwd: def.cwd, envs: def.envs })) {
584
+ await ctx.emit({
585
+ ts: Date.now(),
586
+ workflowName: ctx.workflowName,
587
+ runId: ctx.runId,
588
+ stepIndex: ctx.stepIndex,
589
+ event: "exec_event" /* ExecEvent */,
590
+ payload: ev
591
+ });
592
+ events.push(ev);
593
+ }
594
+ return { ...state, commandEvents: events };
595
+ }
596
+ };
597
+ case "delete_sandbox":
598
+ return {
599
+ id: "delete_sandbox",
600
+ async run(input, ctx) {
601
+ const state = input ?? {};
602
+ if (state.sandboxId) await ctx.control.deleteSandbox(state.sandboxId);
603
+ return { ...state, sandboxId: void 0 };
604
+ }
605
+ };
606
+ case "write_file":
607
+ return {
608
+ id: "write_file",
609
+ async run(input, ctx) {
610
+ const state = input ?? {};
611
+ if (!state.sandboxId) throw new Error("write_file requires sandboxId in workflow state");
612
+ const exec = await ctx.resolveExec(state.sandboxId);
613
+ const content = def.encoding === "base64" ? Buffer.from(def.content, "base64").buffer : def.content;
614
+ await exec.uploadFile(def.path, content);
615
+ return state;
616
+ }
617
+ };
618
+ case "retry": {
619
+ const child = buildStep(def.step);
620
+ return {
621
+ id: "retry",
622
+ rollback: child.rollback,
623
+ async run(input, ctx) {
624
+ let lastErr;
625
+ for (let attempt = 0; attempt < def.maxAttempts; attempt++) {
626
+ try {
627
+ return await child.run(input, ctx);
628
+ } catch (err) {
629
+ lastErr = err;
630
+ if (attempt < def.maxAttempts - 1) {
631
+ const base = def.delayMs ?? 500;
632
+ const delay = def.backoff === "exponential" ? base * Math.pow(2, attempt) : base;
633
+ await ctx.emit({
634
+ ts: Date.now(),
635
+ workflowName: ctx.workflowName,
636
+ runId: ctx.runId,
637
+ stepIndex: ctx.stepIndex,
638
+ event: "exec_event" /* ExecEvent */,
639
+ payload: { type: "retry_attempt", attempt: attempt + 1, maxAttempts: def.maxAttempts, error: String(err) }
640
+ });
641
+ await new Promise((r) => setTimeout(r, delay));
642
+ }
643
+ }
644
+ }
645
+ throw lastErr;
646
+ }
647
+ };
648
+ }
649
+ case "conditional": {
650
+ const thenSteps = def.then.map(buildStep);
651
+ const elseSteps = (def.else ?? []).map(buildStep);
652
+ return {
653
+ id: "conditional",
654
+ async run(input, ctx) {
655
+ const branch = evaluate(def.condition, input) ? thenSteps : elseSteps;
656
+ let current = input;
657
+ for (const step of branch) {
658
+ current = await step.run(current, ctx);
659
+ }
660
+ return current;
661
+ }
662
+ };
663
+ }
664
+ case "loop": {
665
+ return {
666
+ id: "loop",
667
+ async run(input, ctx) {
668
+ const arr = def.items ?? (def.over ? getPath(input, def.over) : void 0);
669
+ if (!Array.isArray(arr)) throw new Error(`loop: must provide either "items" or "over" pointing to an array in workflow state`);
670
+ const runIteration = async (item, index) => {
671
+ const iterState = { ...input, [def.as]: item, loopIndex: index };
672
+ let current = iterState;
673
+ for (const step of def.steps.map(buildStep)) {
674
+ current = await step.run(current, ctx);
675
+ }
676
+ return current;
677
+ };
678
+ const loopResults = def.concurrently ? await Promise.all(arr.map((item, i) => runIteration(item, i))) : await arr.reduce(async (accP, item, i) => {
679
+ const acc = await accP;
680
+ acc.push(await runIteration(item, i));
681
+ return acc;
682
+ }, Promise.resolve([]));
683
+ return { ...input, loopResults };
684
+ }
685
+ };
686
+ }
687
+ case "parallel": {
688
+ return {
689
+ id: "parallel",
690
+ async run(input, ctx) {
691
+ const results = await Promise.all(
692
+ def.steps.map((stepDef, branchIndex) => {
693
+ const branchCtx = {
694
+ ...ctx,
695
+ stepIndex: ctx.stepIndex * 1e3 + branchIndex,
696
+ emit: (entry) => ctx.emit({ ...entry, branch: branchIndex })
697
+ };
698
+ return buildStep(stepDef).run(input, branchCtx);
699
+ })
700
+ );
701
+ const merged = results.reduce(
702
+ (acc, result) => ({ ...acc, ...result }),
703
+ input
704
+ );
705
+ return { ...merged, parallelResults: results };
706
+ }
707
+ };
708
+ }
709
+ case "sequence": {
710
+ const childSteps = def.steps.map(buildStep);
711
+ return {
712
+ id: "sequence",
713
+ async run(input, ctx) {
714
+ let current = input;
715
+ for (const step of childSteps) {
716
+ current = await step.run(current, ctx);
717
+ }
718
+ return current;
719
+ },
720
+ async rollback(input, ctx) {
721
+ for (const step of [...childSteps].reverse()) {
722
+ if (step.rollback) await step.rollback(input, ctx);
723
+ }
724
+ }
725
+ };
726
+ }
727
+ }
728
+ }
729
+
730
+ // src/client.ts
731
+ var DrejError = class extends Error {
732
+ constructor(message, status) {
733
+ super(message);
734
+ this.status = status;
735
+ this.name = "DrejError";
736
+ }
737
+ status;
738
+ };
739
+ var WorkflowRun = class {
740
+ constructor(name, id, _events) {
741
+ this.name = name;
742
+ this.id = id;
743
+ this._events = _events;
744
+ }
745
+ name;
746
+ id;
747
+ _events;
748
+ [Symbol.asyncIterator]() {
749
+ return this._events;
750
+ }
751
+ };
752
+ var DrejClient = class {
753
+ control;
754
+ adapter;
755
+ constructor(options) {
756
+ this.control = new ControlClient({
757
+ baseUrl: options.baseUrl,
758
+ apiKey: options.apiKey ?? ""
759
+ });
760
+ this.adapter = options.adapter;
761
+ }
762
+ /** Call once before first use when using a DB-backed adapter. */
763
+ async connect() {
764
+ await this.adapter.connect?.();
765
+ }
766
+ /** Releases adapter resources (e.g. closes DB connection pool). */
767
+ async close() {
768
+ await this.adapter.close?.();
769
+ }
770
+ // ── Sandbox management ────────────────────────────────────────────────────
771
+ createSandbox(options) {
772
+ return this.control.createSandbox(options);
773
+ }
774
+ listSandboxes(options = {}) {
775
+ return this.control.listSandboxes(options);
776
+ }
777
+ getSandbox(id) {
778
+ return this.control.getSandbox(id);
779
+ }
780
+ deleteSandbox(id) {
781
+ return this.control.deleteSandbox(id);
782
+ }
783
+ pauseSandbox(id) {
784
+ return this.control.pauseSandbox(id);
785
+ }
786
+ resumeSandbox(id) {
787
+ return this.control.resumeSandbox(id);
788
+ }
789
+ renewSandbox(id) {
790
+ return this.control.renewExpiration(id);
791
+ }
792
+ async waitForRunning(id, options = {}) {
793
+ const { timeoutMs = 6e4, pollIntervalMs = 1e3 } = options;
794
+ const deadline = Date.now() + timeoutMs;
795
+ while (Date.now() < deadline) {
796
+ const sandbox = await this.control.getSandbox(id);
797
+ const { state } = sandbox.status;
798
+ if (state === "Running") return sandbox;
799
+ if (state === "Failed" || state === "Terminated") {
800
+ throw new DrejError(`Sandbox ${id} entered state ${state}`, 500);
801
+ }
802
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
803
+ }
804
+ throw new DrejError(`Sandbox ${id} did not reach Running within ${timeoutMs}ms`, 408);
805
+ }
806
+ // ── Snapshot management ───────────────────────────────────────────────────
807
+ createSnapshot(sandboxId) {
808
+ return this.control.createSnapshot(sandboxId);
809
+ }
810
+ listSnapshots(options = {}) {
811
+ return this.control.listSnapshots(options);
812
+ }
813
+ getSnapshot(id) {
814
+ return this.control.getSnapshot(id);
815
+ }
816
+ deleteSnapshot(id) {
817
+ return this.control.deleteSnapshot(id);
818
+ }
819
+ // ── Diagnostics ───────────────────────────────────────────────────────────
820
+ getDiagnosticLogs(sandboxId) {
821
+ return this.control.getDiagnosticLogs(sandboxId);
822
+ }
823
+ getDiagnosticEvents(sandboxId) {
824
+ return this.control.getDiagnosticEvents(sandboxId);
825
+ }
826
+ // ── Workflow execution ────────────────────────────────────────────────────
827
+ async run(w, options) {
828
+ const { name, steps } = w.build();
829
+ const runId = crypto.randomUUID();
830
+ return new WorkflowRun(name, runId, this._execute(name, runId, steps, options));
831
+ }
832
+ async replayFromSnapshot(name, runId, w) {
833
+ const entries = await this.adapter.readAll(name, runId);
834
+ const snapEntry = [...entries].reverse().find((e) => e.event === "snapshot" /* Snapshot */);
835
+ if (!snapEntry) throw new DrejError(`No snapshot found in ledger for ${name}/${runId}`, 404);
836
+ const { snapshotId } = snapEntry.payload;
837
+ const { name: wfName, steps } = w.build();
838
+ const replaySteps = steps.map(
839
+ (s) => s.type === "create_sandbox" ? { ...s, snapshotId } : s
840
+ );
841
+ const replayRunId = crypto.randomUUID();
842
+ return new WorkflowRun(wfName, replayRunId, this._execute(wfName, replayRunId, replaySteps));
843
+ }
844
+ async resumeRun(name, runId, w) {
845
+ const { steps } = w.build();
846
+ const workflowSteps = steps.map(buildStep);
847
+ const stream = this._makeStream(name, runId, async (teeDeps) => {
848
+ const { workflow: workflow2, nextStep, lastOutput } = await Workflow.resumeFromLedger(
849
+ name,
850
+ runId,
851
+ workflowSteps,
852
+ teeDeps
853
+ );
854
+ try {
855
+ await workflow2.run(lastOutput, nextStep);
856
+ } catch {
857
+ try {
858
+ await workflow2.rollback();
859
+ } catch {
860
+ }
861
+ }
862
+ });
863
+ return new WorkflowRun(name, runId, stream);
864
+ }
865
+ // ── Adapter access ────────────────────────────────────────────────────────
866
+ listRuns(workflowName) {
867
+ return this.adapter.listRuns(workflowName);
868
+ }
869
+ getRunLedger(workflowName, runId) {
870
+ return this.adapter.readAll(workflowName, runId);
871
+ }
872
+ // ── Internal ──────────────────────────────────────────────────────────────
873
+ _execute(name, runId, steps, options) {
874
+ return this._makeStream(name, runId, async (teeDeps) => {
875
+ const snapshotHook = options?.snapshotConfig ? {
876
+ async onStepComplete({ workflowName: wfName, runId: rid, stepIndex, output }) {
877
+ if (!shouldSnapshot(options.snapshotConfig, stepIndex)) return;
878
+ const sandboxId = output?.sandboxId;
879
+ if (typeof sandboxId !== "string") return;
880
+ const snap = await teeDeps.control.createSnapshot(sandboxId);
881
+ await waitForSnapshot(teeDeps.control, snap.id);
882
+ await teeDeps.adapter.append({
883
+ ts: Date.now(),
884
+ workflowName: wfName,
885
+ runId: rid,
886
+ stepIndex,
887
+ event: "snapshot" /* Snapshot */,
888
+ payload: { snapshotId: snap.id, sandboxId }
889
+ });
890
+ }
891
+ } : void 0;
892
+ const deps = { ...teeDeps, hooks: snapshotHook };
893
+ const wf = new Workflow(name, runId, steps.map(buildStep), deps);
894
+ try {
895
+ await wf.run({});
896
+ } catch {
897
+ try {
898
+ await wf.rollback();
899
+ } catch {
900
+ }
901
+ }
902
+ });
903
+ }
904
+ // Runs `execute` in the background and returns an async generator that
905
+ // yields WorkflowEvents in real-time as they are appended to the adapter.
906
+ _makeStream(name, runId, execute) {
907
+ const queue = [];
908
+ let wakeup = null;
909
+ let done = false;
910
+ const enqueue = (entry) => {
911
+ queue.push(entry);
912
+ const fn = wakeup;
913
+ wakeup = null;
914
+ fn?.();
915
+ };
916
+ const teeAdapter = {
917
+ append: async (entry) => {
918
+ await this.adapter.append(entry);
919
+ enqueue(entry);
920
+ },
921
+ readAll: (n, id) => this.adapter.readAll(n, id),
922
+ lastCheckpoint: (n, id) => this.adapter.lastCheckpoint(n, id),
923
+ listRuns: (n) => this.adapter.listRuns(n)
924
+ };
925
+ const teeDeps = {
926
+ control: this.control,
927
+ resolveExec: (sandboxId) => resolveExecClient(this.control, sandboxId),
928
+ adapter: teeAdapter
929
+ };
930
+ enqueue({ ts: Date.now(), workflowName: name, runId, stepIndex: -1, event: "run_started" /* RunStarted */, payload: { workflowName: name, runId } });
931
+ execute(teeDeps).finally(() => {
932
+ done = true;
933
+ const fn = wakeup;
934
+ wakeup = null;
935
+ fn?.();
936
+ });
937
+ return (async function* () {
938
+ while (true) {
939
+ while (queue.length > 0) yield queue.shift();
940
+ if (done) break;
941
+ await new Promise((r) => {
942
+ wakeup = r;
943
+ });
944
+ }
945
+ while (queue.length > 0) yield queue.shift();
946
+ })();
947
+ }
948
+ };
949
+
950
+ // src/workflow.ts
951
+ var LoopVar = class {
952
+ constructor(name) {
953
+ this.name = name;
954
+ }
955
+ name;
956
+ toString() {
957
+ return `{{${this.name}}}`;
958
+ }
959
+ };
960
+ function wrapSteps(steps) {
961
+ return steps.length === 1 ? steps[0] : { type: "sequence", steps };
962
+ }
963
+ var SandboxStepBuilder = class _SandboxStepBuilder {
964
+ _steps = [];
965
+ exec(command, opts) {
966
+ this._steps.push({ type: "exec_command", command, ...opts });
967
+ return this;
968
+ }
969
+ writeFile(path, content, encoding) {
970
+ this._steps.push({ type: "write_file", path, content, ...encoding ? { encoding } : {} });
971
+ return this;
972
+ }
973
+ retry(maxAttempts, fn, opts) {
974
+ const inner = new _SandboxStepBuilder();
975
+ fn(inner);
976
+ this._steps.push({ type: "retry", step: wrapSteps(inner.build()), maxAttempts, ...opts });
977
+ return this;
978
+ }
979
+ forEach(source, optsOrFn, fn) {
980
+ const opts = typeof optsOrFn === "function" ? {} : optsOrFn;
981
+ const callback = typeof optsOrFn === "function" ? optsOrFn : fn;
982
+ const varName = opts.as ?? "item";
983
+ const loopVar = new LoopVar(varName);
984
+ const inner = new _SandboxStepBuilder();
985
+ const result = callback(inner, loopVar);
986
+ const steps = typeof result === "string" ? [{ type: "exec_command", command: result }] : result.build();
987
+ this._steps.push({
988
+ type: "loop",
989
+ as: varName,
990
+ steps,
991
+ ...Array.isArray(source) ? { items: source } : { over: source.from },
992
+ ...opts.concurrency !== void 0 && opts.concurrency > 1 ? { concurrently: true } : {}
993
+ });
994
+ return this;
995
+ }
996
+ when(condition, thenFn, elseFn) {
997
+ const thenBuilder = new _SandboxStepBuilder();
998
+ thenFn(thenBuilder);
999
+ const elseSteps = elseFn ? (() => {
1000
+ const b = new _SandboxStepBuilder();
1001
+ elseFn(b);
1002
+ return b.build();
1003
+ })() : void 0;
1004
+ this._steps.push({
1005
+ type: "conditional",
1006
+ condition,
1007
+ then: thenBuilder.build(),
1008
+ ...elseSteps ? { else: elseSteps } : {}
1009
+ });
1010
+ return this;
1011
+ }
1012
+ parallel(fn) {
1013
+ const pb = new SandboxParallelBuilder();
1014
+ fn(pb);
1015
+ this._steps.push({ type: "parallel", steps: pb.build() });
1016
+ return this;
1017
+ }
1018
+ build() {
1019
+ return [...this._steps];
1020
+ }
1021
+ };
1022
+ var SandboxParallelBuilder = class {
1023
+ _branches = [];
1024
+ branch(fn) {
1025
+ const sb = new SandboxStepBuilder();
1026
+ fn(sb);
1027
+ this._branches.push(wrapSteps(sb.build()));
1028
+ return this;
1029
+ }
1030
+ build() {
1031
+ return this._branches;
1032
+ }
1033
+ };
1034
+ var WorkflowParallelBuilder = class {
1035
+ _branches = [];
1036
+ sandbox(opts, fn) {
1037
+ const sb = new SandboxStepBuilder();
1038
+ fn(sb);
1039
+ this._branches.push({
1040
+ type: "sequence",
1041
+ steps: [
1042
+ { type: "create_sandbox", entrypoint: ["tail", "-f", "/dev/null"], ...opts },
1043
+ ...sb.build(),
1044
+ { type: "delete_sandbox" }
1045
+ ]
1046
+ });
1047
+ return this;
1048
+ }
1049
+ branch(fn) {
1050
+ const sb = new SandboxStepBuilder();
1051
+ fn(sb);
1052
+ this._branches.push(wrapSteps(sb.build()));
1053
+ return this;
1054
+ }
1055
+ build() {
1056
+ return this._branches;
1057
+ }
1058
+ };
1059
+ var WorkflowBuilder = class {
1060
+ constructor(_name) {
1061
+ this._name = _name;
1062
+ }
1063
+ _name;
1064
+ _steps = [];
1065
+ sandbox(opts, fn) {
1066
+ const sb = new SandboxStepBuilder();
1067
+ fn(sb);
1068
+ this._steps.push(
1069
+ { type: "create_sandbox", entrypoint: ["tail", "-f", "/dev/null"], ...opts },
1070
+ ...sb.build(),
1071
+ { type: "delete_sandbox" }
1072
+ );
1073
+ return this;
1074
+ }
1075
+ parallel(fn) {
1076
+ const pb = new WorkflowParallelBuilder();
1077
+ fn(pb);
1078
+ this._steps.push({ type: "parallel", steps: pb.build() });
1079
+ return this;
1080
+ }
1081
+ build() {
1082
+ return { name: this._name, steps: this._steps };
1083
+ }
1084
+ };
1085
+ function workflow(name) {
1086
+ return new WorkflowBuilder(name);
1087
+ }
1088
+ export {
1089
+ DrejClient,
1090
+ DrejError,
1091
+ LedgerEvent,
1092
+ SandboxStepBuilder,
1093
+ WorkflowBuilder,
1094
+ WorkflowRun,
1095
+ workflow
1096
+ };